Contact reveal modal: click phone/contact on cards and detail pages
Adds a lazy-loaded contact modal. Any element with data-contact-type + data-contact-id (the «📞 تماس» button on shift/job/talent/recommendation cards, and the contact CTA on the three detail pages) opens a modal that fetches the listing's numbers from a new GET /contact endpoint and renders them with click- to-call links. Numbers are loaded only on click, so they never sit in list-page HTML (privacy / anti-scrape). The endpoint logs the same Apply interest signal for shift/job that the old inline-reveal POST did, and falls back to the facility phone (or Divar source link for talent) when an ad has no own contacts. Verified locally: GET /contact?type=shift&id=1 → {title, contacts:[{value: '021-82032000', href:'tel:...'}]}, and the modal opens and renders on the shift detail page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -390,6 +390,62 @@ app.MapGet("/sitemap.xml", async (HttpContext ctx, AppDbContext db) =>
|
||||
return Results.Content(sb.ToString(), "application/xml");
|
||||
});
|
||||
|
||||
// ---- Contact reveal (modal): a listing's contact channels as JSON, fetched lazily on click so
|
||||
// personal numbers never sit in list-page HTML. Logs the Apply interest signal for shift/job. ----
|
||||
app.MapGet("/contact", async (string? type, int id, AppDbContext db, InterestService interest) =>
|
||||
{
|
||||
object Item(ContactType ct, string value) => new
|
||||
{
|
||||
icon = ContactInfo.Icon(ct), label = ContactInfo.Label(ct), value, href = ContactInfo.Href(ct, value),
|
||||
};
|
||||
|
||||
string? title = null, fallbackUrl = null, fallbackLabel = null;
|
||||
var items = new List<object>();
|
||||
|
||||
switch ((type ?? "").ToLowerInvariant())
|
||||
{
|
||||
case "shift":
|
||||
{
|
||||
var s = await db.Shifts.Include(x => x.Role).Include(x => x.Facility).Include(x => x.Contacts)
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (s is null) return Results.NotFound();
|
||||
title = s.Role?.Name ?? "تماس";
|
||||
items.AddRange(s.Contacts.OrderBy(c => c.SortOrder).Select(c => Item(c.Type, c.Value)));
|
||||
if (items.Count == 0 && !string.IsNullOrWhiteSpace(s.Facility?.Phone)) items.Add(Item(ContactType.Phone, s.Facility!.Phone!));
|
||||
if (!string.IsNullOrWhiteSpace(s.Facility?.BaleId)) items.Add(Item(ContactType.Bale, s.Facility!.BaleId!));
|
||||
await interest.LogAsync(InterestEventType.Apply, id);
|
||||
break;
|
||||
}
|
||||
case "job":
|
||||
{
|
||||
var j = await db.JobOpenings.Include(x => x.Role).Include(x => x.Facility).Include(x => x.Contacts)
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (j is null) return Results.NotFound();
|
||||
title = j.Title;
|
||||
items.AddRange(j.Contacts.OrderBy(c => c.SortOrder).Select(c => Item(c.Type, c.Value)));
|
||||
if (items.Count == 0 && !string.IsNullOrWhiteSpace(j.Facility?.Phone)) items.Add(Item(ContactType.Phone, j.Facility!.Phone!));
|
||||
if (!string.IsNullOrWhiteSpace(j.Facility?.BaleId)) items.Add(Item(ContactType.Bale, j.Facility!.BaleId!));
|
||||
await interest.LogJobAsync(InterestEventType.Apply, id);
|
||||
break;
|
||||
}
|
||||
case "talent":
|
||||
{
|
||||
var t = await db.TalentListings.Include(x => x.Role).Include(x => x.Contacts)
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (t is null) return Results.NotFound();
|
||||
title = string.IsNullOrWhiteSpace(t.PersonName) ? (t.Role?.Name ?? "آماده به کار") : t.PersonName;
|
||||
items.AddRange(t.Contacts.OrderBy(c => c.SortOrder).Select(c => Item(c.Type, c.Value)));
|
||||
if (items.Count == 0 && !string.IsNullOrWhiteSpace(t.Phone)) items.Add(Item(ContactType.Mobile, t.Phone!));
|
||||
if (items.Count == 0 && !string.IsNullOrWhiteSpace(t.SourceUrl)
|
||||
&& Uri.TryCreate(t.SourceUrl, UriKind.Absolute, out var su) && su.Host.Contains("divar"))
|
||||
{ fallbackUrl = t.SourceUrl; fallbackLabel = "مشاهده شماره در دیوار ↗"; }
|
||||
break;
|
||||
}
|
||||
default: return Results.BadRequest();
|
||||
}
|
||||
return Results.Json(new { title, contacts = items, fallbackUrl, fallbackLabel });
|
||||
});
|
||||
|
||||
// ---- Instant search suggestions (typeahead dropdown) ----
|
||||
app.MapGet("/search/suggest", async (string? q, AppDbContext db) =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user