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:
@@ -105,10 +105,8 @@
|
||||
<div class="pay" style="font-size:19px; margin-bottom:6px; color:var(--primary-dark); font-weight:800;">@salary</div>
|
||||
<p class="muted" style="font-size:13px; margin-top:0;">@empLabel</p>
|
||||
<div class="aside-apply">
|
||||
<form method="post">
|
||||
<button type="submit" asp-page-handler="Interest" asp-route-id="@j.Id"
|
||||
class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده راه ارتباطی</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-accent btn-block btn-lg contact-trigger"
|
||||
data-contact-type="job" data-contact-id="@j.Id">📞 اعلام تمایل و مشاهده راه ارتباطی</button>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||
<form method="post" style="flex:1;">
|
||||
@@ -178,26 +176,11 @@
|
||||
|
||||
@* Sticky bottom action bar — mobile only. *@
|
||||
<div class="mobile-action-bar">
|
||||
@if (Model.ShowContact)
|
||||
{
|
||||
@if (!string.IsNullOrEmpty(f.Phone))
|
||||
{
|
||||
<a class="btn btn-accent btn-lg cta-main" href="tel:@f.Phone">📞 تماس با مرکز</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="cta-main center muted" style="align-self:center;">اطلاعات تماس در بالای صفحه</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" class="cta-main">
|
||||
<button type="submit" asp-page-handler="Interest" asp-route-id="@j.Id" class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده تماس</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<button type="submit" asp-page-handler="Save" asp-route-id="@j.Id" class="btn btn-outline btn-lg" aria-label="ذخیره" title="ذخیره">♡</button>
|
||||
</form>
|
||||
}
|
||||
<button type="button" class="btn btn-accent btn-lg cta-main contact-trigger"
|
||||
data-contact-type="job" data-contact-id="@j.Id">📞 اعلام تمایل و مشاهده تماس</button>
|
||||
<form method="post">
|
||||
<button type="submit" asp-page-handler="Save" asp-route-id="@j.Id" class="btn btn-outline btn-lg" aria-label="ذخیره" title="ذخیره">♡</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.MapKey) && Model.Job?.Facility?.Lat is not null)
|
||||
|
||||
@@ -41,6 +41,6 @@
|
||||
}
|
||||
<div class="foot">
|
||||
<span class="pay">@salary</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
<span class="btn btn-accent contact-trigger" style="padding: 6px 14px;" data-contact-type="job" data-contact-id="@Model.Id">📞 تماس</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -275,6 +275,57 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
@* Contact modal — any element with data-contact-type + data-contact-id opens it; numbers are
|
||||
fetched from /contact on click (so they never sit in list HTML and bots can't scrape them). *@
|
||||
<div id="contactModal" class="contact-modal" aria-hidden="true">
|
||||
<div class="contact-modal-box" role="dialog" aria-modal="true" aria-labelledby="contactModalTitle">
|
||||
<div class="contact-modal-head">
|
||||
<h3 id="contactModalTitle">راههای ارتباطی</h3>
|
||||
<button type="button" class="contact-modal-x" data-contact-close aria-label="بستن">✕</button>
|
||||
</div>
|
||||
<div id="contactModalBody" class="contact-modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var modal = document.getElementById('contactModal');
|
||||
var box = document.getElementById('contactModalBody');
|
||||
var titleEl = document.getElementById('contactModalTitle');
|
||||
function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; }
|
||||
function open() { modal.classList.add('show'); modal.setAttribute('aria-hidden', 'false'); }
|
||||
function close() { modal.classList.remove('show'); modal.setAttribute('aria-hidden', 'true'); box.innerHTML = ''; }
|
||||
function render(d) {
|
||||
titleEl.textContent = d.title || 'راههای ارتباطی';
|
||||
var html = '';
|
||||
(d.contacts || []).forEach(function (c) {
|
||||
html += '<div class="contact-row"><span class="c-meta"><span class="c-type">' + esc(c.icon + ' ' + c.label) +
|
||||
'</span><span class="c-val" dir="ltr">' + esc(c.value) + '</span></span>' +
|
||||
(c.href ? '<a class="btn btn-accent" href="' + esc(c.href) + '" target="_blank" rel="nofollow noopener">تماس</a>' : '') + '</div>';
|
||||
});
|
||||
if (d.fallbackUrl) html += '<a class="btn btn-accent btn-block" href="' + esc(d.fallbackUrl) +
|
||||
'" target="_blank" rel="nofollow noopener">' + esc(d.fallbackLabel || 'مشاهده') + '</a>';
|
||||
box.innerHTML = html || '<p class="muted" style="margin:0;">شمارهای ثبت نشده است.</p>';
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var trigger = e.target.closest('[data-contact-type]');
|
||||
if (trigger) {
|
||||
e.preventDefault(); e.stopPropagation(); // don't follow the card link
|
||||
titleEl.textContent = 'راههای ارتباطی';
|
||||
box.innerHTML = '<p class="muted" style="margin:0;">در حال دریافت…</p>';
|
||||
open();
|
||||
fetch('/contact?type=' + encodeURIComponent(trigger.getAttribute('data-contact-type')) +
|
||||
'&id=' + encodeURIComponent(trigger.getAttribute('data-contact-id')))
|
||||
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
||||
.then(render)
|
||||
.catch(function () { box.innerHTML = '<p class="muted" style="margin:0;">خطا در دریافت اطلاعات تماس.</p>'; });
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('[data-contact-close]') || e.target === modal) close();
|
||||
});
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
|
||||
})();
|
||||
</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)
|
||||
|
||||
@@ -35,6 +35,6 @@
|
||||
|
||||
<div class="foot">
|
||||
<span class="pay">@JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
<span class="btn btn-accent contact-trigger" style="padding: 6px 14px;" data-contact-type="shift" data-contact-id="@s.Id">📞 تماس</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -40,6 +40,6 @@
|
||||
<partial name="_HourBar" model="Model" />
|
||||
<div class="foot">
|
||||
<span class="pay">@JalaliDate.PayLabel(Model.PayType, Model.PayAmount, Model.SharePercent)</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
<span class="btn btn-accent contact-trigger" style="padding: 6px 14px;" data-contact-type="shift" data-contact-id="@Model.Id">📞 تماس</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -56,6 +56,6 @@
|
||||
}
|
||||
<div class="foot">
|
||||
<span class="pay">@comp</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">مشاهده و تماس</span>
|
||||
<span class="btn btn-accent contact-trigger" style="padding: 6px 14px;" data-contact-type="talent" data-contact-id="@Model.Id">📞 مشاهده و تماس</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -119,10 +119,8 @@
|
||||
<div class="alert alert-success" style="margin-bottom:12px;">✓ این فرصت ذخیره شد و در پیشنهادهای شما لحاظ میشود.</div>
|
||||
}
|
||||
<div class="aside-apply">
|
||||
<form method="post">
|
||||
<button type="submit" asp-page-handler="Interest" asp-route-id="@s.Id"
|
||||
class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده راه ارتباطی</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-accent btn-block btn-lg contact-trigger"
|
||||
data-contact-type="shift" data-contact-id="@s.Id">📞 اعلام تمایل و مشاهده راه ارتباطی</button>
|
||||
<p class="muted center" style="font-size:12px; margin:8px 0;">با اعلام تمایل، اطلاعات تماس مرکز نمایش داده میشود.</p>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
@@ -196,26 +194,11 @@
|
||||
|
||||
@* Sticky bottom action bar — mobile only. Always-reachable primary action (native-app feel). *@
|
||||
<div class="mobile-action-bar">
|
||||
@if (Model.ShowContact)
|
||||
{
|
||||
@if (!string.IsNullOrEmpty(f.Phone))
|
||||
{
|
||||
<a class="btn btn-accent btn-lg cta-main" href="tel:@f.Phone">📞 تماس با مرکز</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="cta-main center muted" style="align-self:center;">اطلاعات تماس در بالای صفحه</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" class="cta-main">
|
||||
<button type="submit" asp-page-handler="Interest" asp-route-id="@s.Id" class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده تماس</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<button type="submit" asp-page-handler="Save" asp-route-id="@s.Id" class="btn btn-outline btn-lg" aria-label="ذخیره" title="ذخیره">♡</button>
|
||||
</form>
|
||||
}
|
||||
<button type="button" class="btn btn-accent btn-lg cta-main contact-trigger"
|
||||
data-contact-type="shift" data-contact-id="@s.Id">📞 اعلام تمایل و مشاهده تماس</button>
|
||||
<form method="post">
|
||||
<button type="submit" asp-page-handler="Save" asp-route-id="@s.Id" class="btn btn-outline btn-lg" aria-label="ذخیره" title="ذخیره">♡</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.MapKey) && Model.Shift?.Facility?.Lat is not null)
|
||||
|
||||
@@ -13,16 +13,6 @@
|
||||
comp = JalaliDate.Toman(pa) + " مدنظر";
|
||||
else
|
||||
comp = "توافقی";
|
||||
string? telHref = null;
|
||||
if (!string.IsNullOrWhiteSpace(t.Phone))
|
||||
{
|
||||
var digits = new string(t.Phone.Where(char.IsDigit).ToArray());
|
||||
if (digits.Length >= 7) telHref = "tel:" + digits;
|
||||
}
|
||||
// Only Divar is surfaced as a fallback source (and only when no number was extracted).
|
||||
// We never name other crawl sources (medjobs/telegram/…) publicly.
|
||||
bool isDivar = !string.IsNullOrWhiteSpace(t.SourceUrl)
|
||||
&& System.Uri.TryCreate(t.SourceUrl, UriKind.Absolute, out var su) && su.Host.Contains("divar");
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
@@ -67,41 +57,9 @@
|
||||
<aside>
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">راههای ارتباطی</h3>
|
||||
@{ var contacts = (t.Contacts ?? new List<JobsMedical.Web.Models.ContactMethod>()).OrderBy(c => c.SortOrder).ToList(); }
|
||||
@if (contacts.Count > 0)
|
||||
{
|
||||
<div class="contact-reveal">
|
||||
@foreach (var c in contacts)
|
||||
{
|
||||
var href = JobsMedical.Web.Services.ContactInfo.Href(c.Type, c.Value);
|
||||
var label = JobsMedical.Web.Services.ContactInfo.Label(c.Type);
|
||||
var icon = JobsMedical.Web.Services.ContactInfo.Icon(c.Type);
|
||||
var cls = c.Type is JobsMedical.Web.Models.ContactType.Mobile or JobsMedical.Web.Models.ContactType.Phone ? "btn-accent" : "btn-outline";
|
||||
<div class="contact-row">
|
||||
<span class="c-meta"><span class="c-type">@icon @label</span><span class="c-val" dir="ltr">@c.Value</span></span>
|
||||
@if (href is not null)
|
||||
{
|
||||
<a class="btn @cls" href="@href" target="_blank" rel="nofollow noopener">باز کردن</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (telHref is not null)
|
||||
{
|
||||
<a href="@telHref" class="btn btn-accent btn-block btn-lg" dir="ltr">📞 @t.Phone</a>
|
||||
}
|
||||
else if (isDivar)
|
||||
{
|
||||
@* Divar hides the number behind a login-gated reveal — point to the original ad. *@
|
||||
<p class="muted" style="margin-top:0;">مشاهده شماره در وبسایت دیوار</p>
|
||||
<a href="@t.SourceUrl" target="_blank" rel="nofollow noopener" class="btn btn-accent btn-block btn-lg">مشاهده شماره در دیوار ↗</a>
|
||||
<p class="muted" style="font-size:12px; margin:10px 0 0;">برای دریافت شماره به آگهی اصلی در دیوار مراجعه کن.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted">شماره تماس ثبت نشده است.</p>
|
||||
}
|
||||
<button type="button" class="btn btn-accent btn-block btn-lg contact-trigger"
|
||||
data-contact-type="talent" data-contact-id="@t.Id">📞 مشاهده راههای ارتباطی</button>
|
||||
<p class="muted" style="font-size:12px; margin:10px 0 0;">با کلیک، شماره تماس و راههای ارتباطی نمایش داده میشود.</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user