mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 07:11:18 +02:00
search: pagination and scroll, add range counter, mobile version
This commit is contained in:
@@ -4,3 +4,13 @@
|
|||||||
- [x] Add `has_annexes` checkbox to form + DB column
|
- [x] Add `has_annexes` checkbox to form + DB column
|
||||||
- [x] Display duration on admin backoffice recap page
|
- [x] Display duration on admin backoffice recap page
|
||||||
- [x] Update public TFE page duration display for new combined format
|
- [x] Update public TFE page duration display for new combined format
|
||||||
|
- [x] Fix `handleAutosaveResponse is not defined` — load `autosave-handler.js` on about_page edit view
|
||||||
|
- [x] Fix `handleAutosaveResponse is not defined` in admin add/edit — function was scoped inside IIFE bundle, not on window
|
||||||
|
- [x] Fix search result cards: title and author always below image/gradient, remove text overlay on placeholder gradient
|
||||||
|
- [x] Change search results to 10 per page with HTMX pagination (no-JS fallback preserved)
|
||||||
|
- [x] Bump to 15 results/page, make results area vertically scrollable, sticky pagination at viewport bottom
|
||||||
|
- [x] Fix HTMX targetError: move #search-results wrapper inside fragment so it survives outerHTML swaps
|
||||||
|
- [x] Add range counter to results header (e.g. "1–15 sur 112 résultats")
|
||||||
|
- [x] Collapsible filter controls with <details>/<summary> + active filter badge on mobile
|
||||||
|
- [x] Responsive paginator: hide first/last buttons on mobile, smaller touch targets
|
||||||
|
- [x] Compact range counter on mobile: "1–15 / 112" instead of full "1–15 sur 112 résultats"
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ var editor = new OT(document.getElementById('editor'), {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
JS;
|
JS;
|
||||||
} elseif ($editType === 'apropos') {
|
} elseif ($editType === 'apropos' || $editType === 'about_page') {
|
||||||
$extraJs = ["/assets/js/app/autosave-handler.js"];
|
$extraJs = ["/assets/js/app/autosave-handler.js"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,36 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Scrollable results area with sticky pagination ──────────────── */
|
||||||
|
#search-results {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-range--compact {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-xl) var(--space-m);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--step-0);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 6-column index layout ---- */
|
/* ---- 6-column index layout ---- */
|
||||||
@@ -510,7 +540,23 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2xs);
|
gap: var(--space-2xs);
|
||||||
padding: var(--space-m) 0;
|
padding: var(--space-s) 0;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
z-index: 10;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
border-top: 1px solid var(--border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap ul {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn {
|
.pagination-btn {
|
||||||
@@ -546,12 +592,6 @@
|
|||||||
padding: 0 var(--space-2xs);
|
padding: 0 var(--space-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-empty {
|
|
||||||
padding: var(--space-xl) var(--space-m);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--step-0);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Error message */
|
/* Error message */
|
||||||
.search-error {
|
.search-error {
|
||||||
@@ -675,3 +715,133 @@
|
|||||||
color: var(--text-secondary, #888);
|
color: var(--text-secondary, #888);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Collapsible filter controls ─────────────────────────────────── */
|
||||||
|
.search-filters-toggle {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid var(--border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters-summary {
|
||||||
|
display: none; /* hidden on desktop */
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
padding: var(--space-xs) var(--space-m);
|
||||||
|
font-size: var(--step--2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters-summary::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
border-right: 2px solid var(--text-secondary);
|
||||||
|
border-bottom: 2px solid var(--text-secondary);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters-toggle[open] .search-filters-summary::after {
|
||||||
|
transform: rotate(-135deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.4em;
|
||||||
|
height: 1.4em;
|
||||||
|
padding: 0 0.35em;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: var(--accent-foreground);
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive: mobile (≤ 768px) ────────────────────────────────── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* Show filter toggle summary, collapse by default if no filters active */
|
||||||
|
.search-filters-summary {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters-toggle:not([open]) .search-controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters-toggle[open] .search-controls {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters stack vertically on mobile */
|
||||||
|
.search-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
padding: var(--space-xs) var(--space-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filter-label {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--space-3xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filter-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-apply-btn,
|
||||||
|
.search-reset-link {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination: hide first/last edge buttons */
|
||||||
|
.pagination-edge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn {
|
||||||
|
min-width: 2.25rem;
|
||||||
|
min-height: 2.25rem;
|
||||||
|
font-size: var(--step--2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
font-size: var(--step--2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce results grid gaps and padding */
|
||||||
|
.results-grid {
|
||||||
|
gap: var(--space-s);
|
||||||
|
padding: var(--space-2xs) var(--space-s) var(--space-s);
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-header {
|
||||||
|
margin: var(--space-s) var(--space-s) var(--space-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-range {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-range--compact {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ function handleAutosaveResponse(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose globally for HTMX hx-on::after-request inlines (bundles wrap in IIFE)
|
||||||
|
window.handleAutosaveResponse = handleAutosaveResponse;
|
||||||
|
|
||||||
// Show saving indicator while request is in flight
|
// Show saving indicator while request is in flight
|
||||||
document.body.addEventListener("htmx:beforeRequest", (e) => {
|
document.body.addEventListener("htmx:beforeRequest", (e) => {
|
||||||
const url = e.detail.requestConfig?.path || "";
|
const url = e.detail.requestConfig?.path || "";
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class SearchController
|
|||||||
{
|
{
|
||||||
private const RATE_LIMIT_MAX = 300;
|
private const RATE_LIMIT_MAX = 300;
|
||||||
private const RATE_LIMIT_WINDOW = 60; // seconds
|
private const RATE_LIMIT_WINDOW = 60; // seconds
|
||||||
private const ITEMS_PER_PAGE = 30;
|
private const ITEMS_PER_PAGE = 15;
|
||||||
|
|
||||||
private Database $db;
|
private Database $db;
|
||||||
private RateLimit $rateLimit;
|
private RateLimit $rateLimit;
|
||||||
@@ -79,6 +79,7 @@ class SearchController
|
|||||||
$searchParams = $this->collectSearchParams();
|
$searchParams = $this->collectSearchParams();
|
||||||
$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;
|
||||||
|
$isHtmx = !empty($_SERVER['HTTP_HX_REQUEST']);
|
||||||
$validationError = null;
|
$validationError = null;
|
||||||
|
|
||||||
$results = [];
|
$results = [];
|
||||||
@@ -99,11 +100,13 @@ class SearchController
|
|||||||
);
|
);
|
||||||
$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();
|
if (!$isHtmx) {
|
||||||
$orientations = $this->db->getAllOrientations();
|
$years = $this->db->getAvailableYears();
|
||||||
$apPrograms = $this->db->getAllAPPrograms();
|
$orientations = $this->db->getAllOrientations();
|
||||||
$finalityTypes = $this->db->getAllFinalityTypes();
|
$apPrograms = $this->db->getAllAPPrograms();
|
||||||
$formatTypes = $this->db->getAllFormatTypes();
|
$finalityTypes = $this->db->getAllFinalityTypes();
|
||||||
|
$formatTypes = $this->db->getAllFormatTypes();
|
||||||
|
}
|
||||||
if (!empty($results)) {
|
if (!empty($results)) {
|
||||||
$coverMap = $this->db->getCoverPathsForTheses(array_column($results, 'id'));
|
$coverMap = $this->db->getCoverPathsForTheses(array_column($results, 'id'));
|
||||||
}
|
}
|
||||||
@@ -119,6 +122,14 @@ class SearchController
|
|||||||
|
|
||||||
$query = $_GET['query'] ?? '';
|
$query = $_GET['query'] ?? '';
|
||||||
|
|
||||||
|
// HTMX partial: render just the results fragment and exit
|
||||||
|
if ($isHtmx) {
|
||||||
|
$this->renderSearchResultsFragment(compact(
|
||||||
|
'results', 'totalItems', 'totalPages', 'page',
|
||||||
|
'baseParams', 'coverMap', 'validationError',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'searchParams' => $searchParams,
|
'searchParams' => $searchParams,
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
@@ -154,6 +165,7 @@ class SearchController
|
|||||||
],
|
],
|
||||||
'currentNav' => 'repertoire',
|
'currentNav' => 'repertoire',
|
||||||
'extraCss' => ['/assets/dist/repertoire.min.css'],
|
'extraCss' => ['/assets/dist/repertoire.min.css'],
|
||||||
|
'extraJs' => ['/assets/js/vendor/htmx.min.js'],
|
||||||
'bodyClass' => 'search-body',
|
'bodyClass' => 'search-body',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -212,6 +224,18 @@ class SearchController
|
|||||||
|
|
||||||
// ── Private helpers ───────────────────────────────────────────────────────
|
// ── Private helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the search results fragment and exit (for HTMX pagination).
|
||||||
|
* Never returns.
|
||||||
|
*/
|
||||||
|
private function renderSearchResultsFragment(array $vars): never
|
||||||
|
{
|
||||||
|
header('Content-Type: text/html; charset=UTF-8');
|
||||||
|
extract($vars);
|
||||||
|
include APP_ROOT . '/templates/partials/search-results.php';
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the repertoire index partial and exit (for HTMX swaps).
|
* Render the repertoire index partial and exit (for HTMX swaps).
|
||||||
* Never returns.
|
* Never returns.
|
||||||
|
|||||||
@@ -27,34 +27,42 @@ $paginationUrl = static function(int $targetPage) use ($baseParams): string {
|
|||||||
|
|
||||||
$atFirst = $page <= 1;
|
$atFirst = $page <= 1;
|
||||||
$atLast = $page >= $totalPages;
|
$atLast = $page >= $totalPages;
|
||||||
|
|
||||||
|
/** Build HTMX attributes for a non-disabled pagination link. */
|
||||||
|
$hxAttrs = static function(string $url): string {
|
||||||
|
return 'hx-get="' . htmlspecialchars($url) . '"'
|
||||||
|
. ' hx-target="#search-results"'
|
||||||
|
. ' hx-swap="outerHTML"'
|
||||||
|
. ' hx-push-url="' . htmlspecialchars($url) . '"';
|
||||||
|
};
|
||||||
?>
|
?>
|
||||||
<nav class="pagination-wrap" aria-label="Pagination">
|
<nav class="pagination-wrap" aria-label="Pagination">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li class="pagination-edge">
|
||||||
<a href="<?= $paginationUrl(1) ?>"
|
<a href="<?= $firstUrl = $paginationUrl(1) ?>"
|
||||||
class="pagination-btn<?= $atFirst ? ' disabled' : '' ?>"
|
class="pagination-btn<?= $atFirst ? ' disabled' : '' ?>"
|
||||||
<?= $atFirst ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
<?= $atFirst ? 'aria-disabled="true" tabindex="-1"' : $hxAttrs($firstUrl) ?>
|
||||||
aria-label="Première page">«</a>
|
aria-label="Première page">«</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="<?= $paginationUrl(max(1, $page - 1)) ?>"
|
<a href="<?= $prevUrl = $paginationUrl(max(1, $page - 1)) ?>"
|
||||||
class="pagination-btn<?= $atFirst ? ' disabled' : '' ?>"
|
class="pagination-btn<?= $atFirst ? ' disabled' : '' ?>"
|
||||||
<?= $atFirst ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
<?= $atFirst ? 'aria-disabled="true" tabindex="-1"' : $hxAttrs($prevUrl) ?>
|
||||||
aria-label="Page précédente">‹</a>
|
aria-label="Page précédente">‹</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="pagination-info" aria-current="page">
|
<li class="pagination-info" aria-current="page">
|
||||||
<span class="page-current"><?= $page ?></span> / <?= $totalPages ?>
|
<span class="page-current"><?= $page ?></span> / <?= $totalPages ?>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="<?= $paginationUrl(min($totalPages, $page + 1)) ?>"
|
<a href="<?= $nextUrl = $paginationUrl(min($totalPages, $page + 1)) ?>"
|
||||||
class="pagination-btn<?= $atLast ? ' disabled' : '' ?>"
|
class="pagination-btn<?= $atLast ? ' disabled' : '' ?>"
|
||||||
<?= $atLast ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
<?= $atLast ? 'aria-disabled="true" tabindex="-1"' : $hxAttrs($nextUrl) ?>
|
||||||
aria-label="Page suivante">›</a>
|
aria-label="Page suivante">›</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="pagination-edge">
|
||||||
<a href="<?= $paginationUrl($totalPages) ?>"
|
<a href="<?= $lastUrl = $paginationUrl($totalPages) ?>"
|
||||||
class="pagination-btn<?= $atLast ? ' disabled' : '' ?>"
|
class="pagination-btn<?= $atLast ? ' disabled' : '' ?>"
|
||||||
<?= $atLast ? 'aria-disabled="true" tabindex="-1"' : '' ?>
|
<?= $atLast ? 'aria-disabled="true" tabindex="-1"' : $hxAttrs($lastUrl) ?>
|
||||||
aria-label="Dernière page">»</a>
|
aria-label="Dernière page">»</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Search results fragment
|
||||||
|
*
|
||||||
|
* Renders the results header, grid, and pagination.
|
||||||
|
* Used both for the initial page load and for HTMX pagination swaps.
|
||||||
|
*
|
||||||
|
* Required variables:
|
||||||
|
* array $results
|
||||||
|
* int $totalItems
|
||||||
|
* int $totalPages
|
||||||
|
* int $page
|
||||||
|
* array $baseParams
|
||||||
|
* array $coverMap
|
||||||
|
* ?string $validationError
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<div id="search-results">
|
||||||
|
<?php if ($validationError): ?>
|
||||||
|
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$perPage = 15;
|
||||||
|
$from = ($page - 1) * $perPage + 1;
|
||||||
|
$to = min($page * $perPage, $totalItems);
|
||||||
|
?>
|
||||||
|
<output class="search-results-header" role="status">
|
||||||
|
<?php if ($totalItems > 0): ?>
|
||||||
|
<span class="search-results-range"><?= $from ?>–<?= $to ?> sur <?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></span>
|
||||||
|
<span class="search-results-range--compact"><?= $from ?>–<?= $to ?> / <?= $totalItems ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span>0 résultat</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<?php if (!empty($results)): ?>
|
||||||
|
<ul class="results-grid">
|
||||||
|
<?php foreach ($results as $item): ?>
|
||||||
|
<?php $thumb = $coverMap[$item['id']] ?? null; ?>
|
||||||
|
<li><a href="/tfe?id=<?= (int)$item['id'] ?>" class="result-card">
|
||||||
|
<?php if ($thumb): ?>
|
||||||
|
<figure class="result-card__cover">
|
||||||
|
<img src="/media?path=<?= urlencode($thumb) ?>"
|
||||||
|
alt="Couverture — <?= htmlspecialchars($item['title']) ?>"
|
||||||
|
loading="lazy">
|
||||||
|
</figure>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="result-card__gradient"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="result-card__authors"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
|
||||||
|
<span class="result-card__title"><?= htmlspecialchars($item['title']) ?></span>
|
||||||
|
<small class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></small>
|
||||||
|
</a></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="search-empty">Aucun résultat pour cette recherche.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
@@ -2,105 +2,89 @@
|
|||||||
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
|
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Count active filters (excluding 'query' and 'page')
|
||||||
|
$filterKeys = ['year', 'ap_program', 'orientation', 'finality', 'format'];
|
||||||
|
$activeFilterCount = 0;
|
||||||
|
foreach ($filterKeys as $k) {
|
||||||
|
if (!empty($_GET[$k])) $activeFilterCount++;
|
||||||
|
}
|
||||||
|
?>
|
||||||
<!-- Filter controls -->
|
<!-- Filter controls -->
|
||||||
<form class="search-controls" method="GET" action="/search">
|
<details class="search-filters-toggle" open>
|
||||||
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
|
<summary class="search-filters-summary">
|
||||||
|
Filtres
|
||||||
|
<?php if ($activeFilterCount > 0): ?>
|
||||||
|
<span class="search-filters-badge"><?= $activeFilterCount ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</summary>
|
||||||
|
<form class="search-controls" method="GET" action="/search">
|
||||||
|
<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
|
||||||
<select class="search-filter-select" name="year" id="filter-year">
|
<select class="search-filter-select" name="year" id="filter-year">
|
||||||
<option value="">Toutes</option>
|
<option value="">Toutes</option>
|
||||||
<?php foreach ($years as $y): ?>
|
<?php foreach ($years as $y): ?>
|
||||||
<option value="<?= (int)$y ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
|
<option value="<?= (int)$y ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
|
||||||
<?= (int)$y ?>
|
<?= (int)$y ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="search-filter-label" for="filter-ap">AP
|
<label class="search-filter-label" for="filter-ap">AP
|
||||||
<select class="search-filter-select" name="ap_program" id="filter-ap">
|
<select class="search-filter-select" name="ap_program" id="filter-ap">
|
||||||
<option value="">Tous</option>
|
<option value="">Tous</option>
|
||||||
<?php foreach ($apPrograms as $ap): ?>
|
<?php foreach ($apPrograms as $ap): ?>
|
||||||
<option value="<?= htmlspecialchars($ap['name']) ?>"
|
<option value="<?= htmlspecialchars($ap['name']) ?>"
|
||||||
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'selected' : '' ?>>
|
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'selected' : '' ?>>
|
||||||
<?= htmlspecialchars($ap['name']) ?>
|
<?= htmlspecialchars($ap['name']) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="search-filter-label" for="filter-orientation">Orientation
|
<label class="search-filter-label" for="filter-orientation">Orientation
|
||||||
<select class="search-filter-select" name="orientation" id="filter-orientation">
|
<select class="search-filter-select" name="orientation" id="filter-orientation">
|
||||||
<option value="">Toutes</option>
|
<option value="">Toutes</option>
|
||||||
<?php foreach ($orientations as $o): ?>
|
<?php foreach ($orientations as $o): ?>
|
||||||
<option value="<?= htmlspecialchars($o['name']) ?>"
|
<option value="<?= htmlspecialchars($o['name']) ?>"
|
||||||
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'selected' : '' ?>>
|
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'selected' : '' ?>>
|
||||||
<?= htmlspecialchars($o['name']) ?>
|
<?= htmlspecialchars($o['name']) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="search-filter-label" for="filter-finality">Finalité
|
<label class="search-filter-label" for="filter-finality">Finalité
|
||||||
<select class="search-filter-select" name="finality" id="filter-finality">
|
<select class="search-filter-select" name="finality" id="filter-finality">
|
||||||
<option value="">Toutes</option>
|
<option value="">Toutes</option>
|
||||||
<?php foreach ($finalityTypes as $f): ?>
|
<?php foreach ($finalityTypes as $f): ?>
|
||||||
<option value="<?= htmlspecialchars($f['name']) ?>"
|
<option value="<?= htmlspecialchars($f['name']) ?>"
|
||||||
<?= (isset($_GET['finality']) && $_GET['finality'] == $f['name']) ? 'selected' : '' ?>>
|
<?= (isset($_GET['finality']) && $_GET['finality'] == $f['name']) ? 'selected' : '' ?>>
|
||||||
<?= htmlspecialchars($f['name']) ?>
|
<?= htmlspecialchars($f['name']) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="search-filter-label" for="filter-format">Format
|
<label class="search-filter-label" for="filter-format">Format
|
||||||
<select class="search-filter-select" name="format" id="filter-format">
|
<select class="search-filter-select" name="format" id="filter-format">
|
||||||
<option value="">Tous</option>
|
<option value="">Tous</option>
|
||||||
<?php foreach ($formatTypes as $fmt): ?>
|
<?php foreach ($formatTypes as $fmt): ?>
|
||||||
<option value="<?= htmlspecialchars($fmt['name']) ?>"
|
<option value="<?= htmlspecialchars($fmt['name']) ?>"
|
||||||
<?= (isset($_GET['format']) && $_GET['format'] == $fmt['name']) ? 'selected' : '' ?>>
|
<?= (isset($_GET['format']) && $_GET['format'] == $fmt['name']) ? 'selected' : '' ?>>
|
||||||
<?= htmlspecialchars($fmt['name']) ?>
|
<?= htmlspecialchars($fmt['name']) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="submit" class="btn btn--primary btn--sm search-apply-btn">Filtrer</button>
|
<button type="submit" class="btn btn--primary btn--sm search-apply-btn">Filtrer</button>
|
||||||
<a href="/search?query=<?= urlencode($_GET['query'] ?? '') ?>" class="btn btn--secondary btn--sm search-reset-link">Réinitialiser</a>
|
<a href="/search?query=<?= urlencode($_GET['query'] ?? '') ?>" class="btn btn--secondary btn--sm search-reset-link">Réinitialiser</a>
|
||||||
</form>
|
</form>
|
||||||
|
</details>
|
||||||
|
|
||||||
<main class="search-main" id="main-content">
|
<main class="search-main" id="main-content">
|
||||||
<output class="search-results-header" role="status"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></output>
|
<?php include APP_ROOT . '/templates/partials/search-results.php'; ?>
|
||||||
|
|
||||||
<?php if (!empty($results)): ?>
|
|
||||||
<ul class="results-grid">
|
|
||||||
<?php foreach ($results as $item): ?>
|
|
||||||
<?php $thumb = $coverMap[$item['id']] ?? null; ?>
|
|
||||||
<li><a href="/tfe?id=<?= (int)$item['id'] ?>" class="result-card">
|
|
||||||
<?php if ($thumb): ?>
|
|
||||||
<figure class="result-card__cover">
|
|
||||||
<img src="/media?path=<?= urlencode($thumb) ?>"
|
|
||||||
alt="Couverture — <?= htmlspecialchars($item['title']) ?>"
|
|
||||||
loading="lazy">
|
|
||||||
</figure>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="result-card__gradient">
|
|
||||||
<span class="result-card__gradient-author"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
|
|
||||||
<span class="result-card__gradient-title"><?= htmlspecialchars($item['title']) ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($thumb): ?>
|
|
||||||
<span class="result-card__authors"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<span class="result-card__title"><?= htmlspecialchars($item['title']) ?></span>
|
|
||||||
<small class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></small>
|
|
||||||
</a></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
|
|
||||||
|
|
||||||
<?php else: ?>
|
|
||||||
<p class="search-empty">Aucun résultat pour cette recherche.</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user