diff --git a/TODO.md b/TODO.md index 22eac9b..6e931f5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ # Posterg TODO ## Features -- [x] Student name popover preview in /repertoire +- [x] Student name popover preview in /repertoire (zero per-hover requests) - [x] `Database::getThesesByAuthorName()` query - [x] `SearchController::handleStudentPreview()` HTMX endpoint - [x] `/repertoire/student-preview` route in Dispatcher diff --git a/app/src/Controllers/SearchController.php b/app/src/Controllers/SearchController.php index fccbe6d..1b8c046 100644 --- a/app/src/Controllers/SearchController.php +++ b/app/src/Controllers/SearchController.php @@ -194,6 +194,7 @@ class SearchController "currentNav" => "repertoire", "extraCss" => ["/assets/css/repertoire.css"], "bodyClass" => "search-body", + "db" => $this->db, ]; } @@ -233,6 +234,7 @@ class SearchController ): never { header("Content-Type: text/html; charset=UTF-8"); $isHtmx = true; + $db = $this->db; include APP_ROOT . "/templates/partials/repertoire-index.php"; exit(); } diff --git a/app/src/Database.php b/app/src/Database.php index 1f0b5a3..b4152f0 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -473,6 +473,37 @@ class Database { return $stmt->fetchAll(); } + /** + * Batch variant: fetch preview data for a list of author names in one query. + * Returns [ authorName => [ thesis, ... ], ... ] + * + * @param string[] $names + * @return array + */ + public function getThesesForAuthors(array $names): array { + if (empty($names)) return []; + + $placeholders = implode(',', array_fill(0, count($names), '?')); + $stmt = $this->pdo->prepare( + "SELECT a.name AS author_name, + vp.id, vp.title, vp.subtitle, vp.year, vp.synopsis, + vp.orientation, vp.finality_type, vp.banner_path, vp.authors + FROM v_theses_public vp + JOIN thesis_authors ta ON ta.thesis_id = vp.id + JOIN authors a ON a.id = ta.author_id + WHERE a.name IN ($placeholders) + ORDER BY a.name ASC, vp.year DESC, vp.title ASC" + ); + $stmt->execute($names); + $rows = $stmt->fetchAll(); + + $grouped = []; + foreach ($rows as $row) { + $grouped[$row['author_name']][] = $row; + } + return $grouped; + } + public function getAvailableYears() { $sql = "SELECT DISTINCT year FROM theses WHERE is_published = 1 ORDER BY year DESC"; $stmt = $this->pdo->query($sql); diff --git a/app/templates/partials/repertoire-index.php b/app/templates/partials/repertoire-index.php index 1c41b4c..bc99c9d 100644 --- a/app/templates/partials/repertoire-index.php +++ b/app/templates/partials/repertoire-index.php @@ -31,6 +31,11 @@ ksort($studentWorks); // Legacy alias for single-id use $studentMap = array_map(fn($ids) => $ids[0], $studentWorks); +// Batch-fetch all preview data for visible students (one query) +$previewsByAuthor = !empty($studentWorks) + ? $db->getThesesForAuthors(array_keys($studentWorks)) + : []; + /** * Build the toggle URL for a filter button. * Toggles $value in $dim; keeps all other active filters intact. @@ -178,17 +183,14 @@ $hx = 'hx-target="#repertoire-index" hx-swap="outerHTML" hx-push-url="true" hx-i $ids): ?>
  • + data-student-name="" + data-preview="">
  • @@ -222,3 +224,38 @@ $hx = 'hx-target="#repertoire-index" hx-swap="outerHTML" hx-push-url="true" hx-i + + $ids): + $templateId = 'sp-' . md5($name); + $theses = $previewsByAuthor[$name] ?? []; + if (empty($theses)) continue; +?> + + diff --git a/app/templates/public/repertoire.php b/app/templates/public/repertoire.php index d489f7d..4d404a8 100644 --- a/app/templates/public/repertoire.php +++ b/app/templates/public/repertoire.php @@ -12,49 +12,50 @@ var popover = document.getElementById('student-popover'); var currentAnchor = null; - // Position the popover next to the hovered student entry function positionPopover(anchor) { - var rect = anchor.getBoundingClientRect(); + var rect = anchor.getBoundingClientRect(); var scrollY = window.scrollY || 0; var scrollX = window.scrollX || 0; - // Place to the right of the column; fall back left if off-screen var left = rect.right + scrollX + 12; var top = rect.top + scrollY; - if (left + 320 > window.innerWidth + scrollX) { - left = rect.left + scrollX - 332; + if (left + 300 > window.innerWidth + scrollX) { + left = rect.left + scrollX - 312; } popover.style.left = left + 'px'; popover.style.top = top + 'px'; } - // Show popover after HTMX fills it - document.body.addEventListener('htmx:afterSwap', function (e) { - if (e.detail.target !== popover) return; - if (!popover.innerHTML.trim()) return; + function showPreview(anchor) { + var tplId = anchor.dataset.preview; + if (!tplId) return; + var tpl = document.getElementById(tplId); + if (!tpl) return; + popover.innerHTML = ''; + popover.appendChild(tpl.content.cloneNode(true)); + positionPopover(anchor); popover.hidden = false; - if (currentAnchor) positionPopover(currentAnchor); - }); + } + + function hidePreview() { + popover.hidden = true; + currentAnchor = null; + } - // Track hovered anchor document.body.addEventListener('mouseenter', function (e) { - var a = e.target.closest('[data-student-name]'); + var a = e.target.closest('[data-preview]'); if (!a) return; currentAnchor = a; + showPreview(a); }, true); - // Hide when leaving BOTH the anchor and the popover document.body.addEventListener('mouseleave', function (e) { - var a = e.target.closest('[data-student-name]'); + var a = e.target.closest('[data-preview]'); var p = e.target.closest('#student-popover'); if (!a && !p) return; - // Small delay so mouse can move into popover setTimeout(function () { - var hoverAnchor = document.querySelector('[data-student-name]:hover'); - var hoverPop = document.querySelector('#student-popover:hover'); - if (!hoverAnchor && !hoverPop) { - popover.hidden = true; - popover.innerHTML = ''; - currentAnchor = null; + if (!document.querySelector('[data-preview]:hover') && + !document.querySelector('#student-popover:hover')) { + hidePreview(); } }, 120); }, true);