Search: fix header UI + instant typeahead (5 highlighted matches) + ranking
- 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:
@@ -359,4 +359,44 @@ app.MapGet("/sitemap.xml", async (HttpContext ctx, AppDbContext db) =>
|
||||
return Results.Content(sb.ToString(), "application/xml");
|
||||
});
|
||||
|
||||
// ---- Instant search suggestions (typeahead dropdown) ----
|
||||
app.MapGet("/search/suggest", async (string? q, AppDbContext db) =>
|
||||
{
|
||||
var term = (q ?? "").Trim();
|
||||
if (term.Length < 2) return Results.Json(Array.Empty<SuggestItem>());
|
||||
var like = $"%{term}%";
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var jobCut = JobsMedical.Web.Services.Scraping.ListingPolicy.JobCutoffUtc;
|
||||
var talentCut = JobsMedical.Web.Services.Scraping.ListingPolicy.TalentCutoffUtc;
|
||||
|
||||
var shifts = await db.Shifts
|
||||
.Where(s => s.Status == ShiftStatus.Open && s.Date >= today &&
|
||||
(EF.Functions.ILike(s.Facility.Name, like) || EF.Functions.ILike(s.Role.Name, like) || EF.Functions.ILike(s.SpecialtyRequired, like)))
|
||||
.OrderByDescending(s => s.CreatedAt).Take(5)
|
||||
.Select(s => new SuggestItem("شیفت", s.Role.Name + " — " + s.Facility.Name, "/Shifts/Details/" + s.Id)).ToListAsync();
|
||||
var jobs = await db.JobOpenings
|
||||
.Where(j => j.Status == ShiftStatus.Open && j.CreatedAt >= jobCut &&
|
||||
(EF.Functions.ILike(j.Title, like) || EF.Functions.ILike(j.Facility.Name, like) || EF.Functions.ILike(j.Role.Name, like)))
|
||||
.OrderByDescending(j => j.CreatedAt).Take(5)
|
||||
.Select(j => new SuggestItem("استخدام", j.Title, "/Jobs/Details/" + j.Id)).ToListAsync();
|
||||
var talent = await db.TalentListings
|
||||
.Where(t => t.Status == ShiftStatus.Open && t.CreatedAt >= talentCut &&
|
||||
(EF.Functions.ILike(t.Tags ?? "", like) || EF.Functions.ILike(t.Role.Name, like) || EF.Functions.ILike(t.City.Name, like) || EF.Functions.ILike(t.PersonName ?? "", like)))
|
||||
.OrderByDescending(t => t.CreatedAt).Take(5)
|
||||
.Select(t => new SuggestItem("آمادهبهکار", (t.PersonName ?? t.Role.Name) + " — " + t.City.Name, "/Talent/Details/" + t.Id)).ToListAsync();
|
||||
|
||||
// round-robin merge so all three types appear, capped at 5
|
||||
var merged = new List<SuggestItem>();
|
||||
for (var i = 0; i < 5 && merged.Count < 5; i++)
|
||||
{
|
||||
if (i < shifts.Count) merged.Add(shifts[i]);
|
||||
if (merged.Count < 5 && i < jobs.Count) merged.Add(jobs[i]);
|
||||
if (merged.Count < 5 && i < talent.Count) merged.Add(talent[i]);
|
||||
}
|
||||
return Results.Json(merged.Take(5));
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
/// <summary>One typeahead suggestion row (lowercase props → camelCase JSON for the client).</summary>
|
||||
public record SuggestItem(string type, string label, string url);
|
||||
|
||||
Reference in New Issue
Block a user