Contact reveal modal: click phone/contact on cards and detail pages
CI/CD / CI · dotnet build (push) Successful in 2m26s
CI/CD / Deploy · hamkadr (push) Successful in 58s

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:
soroush.asadi
2026-06-20 09:04:08 +03:30
parent 0cf5b30dd8
commit 4c0b29addf
10 changed files with 144 additions and 97 deletions
@@ -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>