Search: fix header UI + instant typeahead (5 highlighted matches) + ranking
CI/CD / CI · dotnet build (push) Successful in 1m38s
CI/CD / Deploy · hamkadr (push) Successful in 2m6s

- Header search restyled as one clean RTL pill (input + button flush).
- Google-style autocomplete: typing ≥2 chars fetches /search/suggest and
  shows up to 5 live matches (round-robin across shifts/jobs/applicants)
  with the query highlighted, plus a «همه نتایج» link. Debounced, closes on
  outside-click/Escape.
- Search results page now RANKS by relevance (term hits in role/title/
  facility/city/tags weighted ×3, description ×1) instead of date-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-08 11:58:30 +03:30
parent 9db4deafbc
commit 61afc957aa
4 changed files with 132 additions and 10 deletions
@@ -226,6 +226,51 @@
});
</script>
@* Instant search suggestions (typeahead) for the header search box. *@
<script>
(function () {
var form = document.querySelector('.nav-search');
if (!form) return;
var input = form.querySelector('input');
var box = document.createElement('div');
box.className = 'nav-search-results';
box.style.display = 'none';
form.appendChild(box);
var timer;
function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; }
function hi(text, q) {
var safe = esc(text);
var terms = q.split(/\s+/).filter(function (t) { return t.length >= 2; })
.map(function (t) { return t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); });
if (!terms.length) return safe;
try { return safe.replace(new RegExp('(' + terms.join('|') + ')', 'gi'), '<mark>$1</mark>'); }
catch (e) { return safe; }
}
function hide() { box.style.display = 'none'; box.innerHTML = ''; }
input.addEventListener('input', function () {
var q = input.value.trim();
clearTimeout(timer);
if (q.length < 2) { hide(); return; }
timer = setTimeout(function () {
fetch('/search/suggest?q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(function (items) {
if (!items || !items.length) { hide(); return; }
var html = items.map(function (it) {
return '<a href="' + it.url + '"><span class="ns-type">' + esc(it.type) +
'</span><span class="ns-label">' + hi(it.label, q) + '</span></a>';
}).join('');
html += '<a class="ns-all" href="/Search?Q=' + encodeURIComponent(q) + '">همه نتایج برای «' + esc(q) + '» ←</a>';
box.innerHTML = html;
box.style.display = 'block';
}).catch(function () { hide(); });
}, 200);
});
document.addEventListener('click', function (e) { if (!form.contains(e.target)) hide(); });
input.addEventListener('keydown', function (e) { if (e.key === 'Escape') hide(); });
})();
</script>
@* Live in-app notifications over SSE (our own origin — works in Iran, no Google push).
Updates the bell badge, shows a toast, and fires a local OS notification when allowed. *@
@if (User.Identity?.IsAuthenticated == true)