diff --git a/TODO.md b/TODO.md index e8c2725..7f9298f 100644 --- a/TODO.md +++ b/TODO.md @@ -4,3 +4,13 @@ - [x] Add `has_annexes` checkbox to form + DB column - [x] Display duration on admin backoffice recap page - [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 / + 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" diff --git a/app/public/admin/contenus-edit.php b/app/public/admin/contenus-edit.php index 40305b6..c8169cb 100644 --- a/app/public/admin/contenus-edit.php +++ b/app/public/admin/contenus-edit.php @@ -113,7 +113,7 @@ var editor = new OT(document.getElementById('editor'), { } }); JS; -} elseif ($editType === 'apropos') { +} elseif ($editType === 'apropos' || $editType === 'about_page') { $extraJs = ["/assets/js/app/autosave-handler.js"]; } diff --git a/app/public/assets/css/repertoire.css b/app/public/assets/css/repertoire.css index 02029ea..ce871db 100644 --- a/app/public/assets/css/repertoire.css +++ b/app/public/assets/css/repertoire.css @@ -8,6 +8,36 @@ min-height: 0; overflow: hidden; 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 ---- */ @@ -510,7 +540,23 @@ justify-content: center; align-items: center; 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 { @@ -546,12 +592,6 @@ 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 */ .search-error { @@ -675,3 +715,133 @@ color: var(--text-secondary, #888); 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; + } +} diff --git a/app/public/assets/js/app/autosave-handler.js b/app/public/assets/js/app/autosave-handler.js index 45e0d96..e1d6085 100644 --- a/app/public/assets/js/app/autosave-handler.js +++ b/app/public/assets/js/app/autosave-handler.js @@ -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 document.body.addEventListener("htmx:beforeRequest", (e) => { const url = e.detail.requestConfig?.path || ""; diff --git a/app/src/Controllers/SearchController.php b/app/src/Controllers/SearchController.php index 00d8b15..4d6a2c0 100644 --- a/app/src/Controllers/SearchController.php +++ b/app/src/Controllers/SearchController.php @@ -24,7 +24,7 @@ class SearchController { private const RATE_LIMIT_MAX = 300; private const RATE_LIMIT_WINDOW = 60; // seconds - private const ITEMS_PER_PAGE = 30; + private const ITEMS_PER_PAGE = 15; private Database $db; private RateLimit $rateLimit; @@ -79,6 +79,7 @@ class SearchController $searchParams = $this->collectSearchParams(); $page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1; $offset = ($page - 1) * self::ITEMS_PER_PAGE; + $isHtmx = !empty($_SERVER['HTTP_HX_REQUEST']); $validationError = null; $results = []; @@ -99,11 +100,13 @@ class SearchController ); $totalItems = $this->db->countSearchResults($searchParams); $totalPages = (int) ceil($totalItems / self::ITEMS_PER_PAGE); - $years = $this->db->getAvailableYears(); - $orientations = $this->db->getAllOrientations(); - $apPrograms = $this->db->getAllAPPrograms(); - $finalityTypes = $this->db->getAllFinalityTypes(); - $formatTypes = $this->db->getAllFormatTypes(); + if (!$isHtmx) { + $years = $this->db->getAvailableYears(); + $orientations = $this->db->getAllOrientations(); + $apPrograms = $this->db->getAllAPPrograms(); + $finalityTypes = $this->db->getAllFinalityTypes(); + $formatTypes = $this->db->getAllFormatTypes(); + } if (!empty($results)) { $coverMap = $this->db->getCoverPathsForTheses(array_column($results, 'id')); } @@ -119,6 +122,14 @@ class SearchController $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 [ 'searchParams' => $searchParams, 'page' => $page, @@ -154,6 +165,7 @@ class SearchController ], 'currentNav' => 'repertoire', 'extraCss' => ['/assets/dist/repertoire.min.css'], + 'extraJs' => ['/assets/js/vendor/htmx.min.js'], 'bodyClass' => 'search-body', ]; } @@ -212,6 +224,18 @@ class SearchController // ── 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). * Never returns. diff --git a/app/templates/partials/pagination.php b/app/templates/partials/pagination.php index 586bf4a..6b81e95 100644 --- a/app/templates/partials/pagination.php +++ b/app/templates/partials/pagination.php @@ -27,34 +27,42 @@ $paginationUrl = static function(int $targetPage) use ($baseParams): string { $atFirst = $page <= 1; $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) . '"'; +}; ?> - - + + = $atFirst ? 'aria-disabled="true" tabindex="-1"' : $hxAttrs($firstUrl) ?> aria-label="Première page">« - + = $atFirst ? 'aria-disabled="true" tabindex="-1"' : $hxAttrs($prevUrl) ?> aria-label="Page précédente">‹ = $page ?> / = $totalPages ?> - + = $atLast ? 'aria-disabled="true" tabindex="-1"' : $hxAttrs($nextUrl) ?> aria-label="Page suivante">› - - + + = $atLast ? 'aria-disabled="true" tabindex="-1"' : $hxAttrs($lastUrl) ?> aria-label="Dernière page">» diff --git a/app/templates/partials/search-results.php b/app/templates/partials/search-results.php new file mode 100644 index 0000000..f0428bb --- /dev/null +++ b/app/templates/partials/search-results.php @@ -0,0 +1,63 @@ + + + +⚠ = htmlspecialchars($validationError) ?> + + + + + 0): ?> + = $from ?>–= $to ?> sur = $totalItems ?> résultat= $totalItems > 1 ? 's' : '' ?> + = $from ?>–= $to ?> / = $totalItems ?> + + 0 résultat + + + + + + + + + + + + + + + + = htmlspecialchars($item['authors'] ?? '') ?> + = htmlspecialchars($item['title']) ?> + = htmlspecialchars($item['year']) ?> · = htmlspecialchars($item['orientation']) ?> + + + + + + + +Aucun résultat pour cette recherche. + + diff --git a/app/templates/public/search.php b/app/templates/public/search.php index ec06029..2a07d9f 100644 --- a/app/templates/public/search.php +++ b/app/templates/public/search.php @@ -2,105 +2,89 @@ ⚠ = htmlspecialchars($validationError) ?> + - - + + + Filtres + 0): ?> + = $activeFilterCount ?> + + + + - Année - - Toutes - - > - = (int)$y ?> - - - - + Année + + Toutes + + > + = (int)$y ?> + + + + - AP - - Tous - - > - = htmlspecialchars($ap['name']) ?> - - - - + AP + + Tous + + > + = htmlspecialchars($ap['name']) ?> + + + + - Orientation - - Toutes - - > - = htmlspecialchars($o['name']) ?> - - - - + Orientation + + Toutes + + > + = htmlspecialchars($o['name']) ?> + + + + - Finalité - - Toutes - - > - = htmlspecialchars($f['name']) ?> - - - - + Finalité + + Toutes + + > + = htmlspecialchars($f['name']) ?> + + + + - Format - - Tous - - > - = htmlspecialchars($fmt['name']) ?> - - - - + Format + + Tous + + > + = htmlspecialchars($fmt['name']) ?> + + + + - Filtrer - Réinitialiser - + Filtrer + Réinitialiser + + - = $totalItems ?> résultat= $totalItems > 1 ? 's' : '' ?> - - - - - - - - - - - - - = htmlspecialchars($item['authors'] ?? '') ?> - = htmlspecialchars($item['title']) ?> - - - - = htmlspecialchars($item['authors'] ?? '') ?> - - = htmlspecialchars($item['title']) ?> - = htmlspecialchars($item['year']) ?> · = htmlspecialchars($item['orientation']) ?> - - - - - - - - Aucun résultat pour cette recherche. - +
Aucun résultat pour cette recherche.