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>
|
<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>
|
<p class="muted" style="font-size:13px; margin-top:0;">@empLabel</p>
|
||||||
<div class="aside-apply">
|
<div class="aside-apply">
|
||||||
<form method="post">
|
<button type="button" class="btn btn-accent btn-block btn-lg contact-trigger"
|
||||||
<button type="submit" asp-page-handler="Interest" asp-route-id="@j.Id"
|
data-contact-type="job" data-contact-id="@j.Id">📞 اعلام تمایل و مشاهده راه ارتباطی</button>
|
||||||
class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده راه ارتباطی</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||||
<form method="post" style="flex:1;">
|
<form method="post" style="flex:1;">
|
||||||
@@ -178,26 +176,11 @@
|
|||||||
|
|
||||||
@* Sticky bottom action bar — mobile only. *@
|
@* Sticky bottom action bar — mobile only. *@
|
||||||
<div class="mobile-action-bar">
|
<div class="mobile-action-bar">
|
||||||
@if (Model.ShowContact)
|
<button type="button" class="btn btn-accent btn-lg cta-main contact-trigger"
|
||||||
{
|
data-contact-type="job" data-contact-id="@j.Id">📞 اعلام تمایل و مشاهده تماس</button>
|
||||||
@if (!string.IsNullOrEmpty(f.Phone))
|
<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>
|
||||||
<a class="btn btn-accent btn-lg cta-main" href="tel:@f.Phone">📞 تماس با مرکز</a>
|
</form>
|
||||||
}
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(Model.MapKey) && Model.Job?.Facility?.Lat is not null)
|
@if (!string.IsNullOrEmpty(Model.MapKey) && Model.Job?.Facility?.Lat is not null)
|
||||||
|
|||||||
@@ -41,6 +41,6 @@
|
|||||||
}
|
}
|
||||||
<div class="foot">
|
<div class="foot">
|
||||||
<span class="pay">@salary</span>
|
<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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -275,6 +275,57 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</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).
|
@* 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. *@
|
Updates the bell badge, shows a toast, and fires a local OS notification when allowed. *@
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
|
|||||||
@@ -35,6 +35,6 @@
|
|||||||
|
|
||||||
<div class="foot">
|
<div class="foot">
|
||||||
<span class="pay">@JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)</span>
|
<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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -40,6 +40,6 @@
|
|||||||
<partial name="_HourBar" model="Model" />
|
<partial name="_HourBar" model="Model" />
|
||||||
<div class="foot">
|
<div class="foot">
|
||||||
<span class="pay">@JalaliDate.PayLabel(Model.PayType, Model.PayAmount, Model.SharePercent)</span>
|
<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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -56,6 +56,6 @@
|
|||||||
}
|
}
|
||||||
<div class="foot">
|
<div class="foot">
|
||||||
<span class="pay">@comp</span>
|
<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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -119,10 +119,8 @@
|
|||||||
<div class="alert alert-success" style="margin-bottom:12px;">✓ این فرصت ذخیره شد و در پیشنهادهای شما لحاظ میشود.</div>
|
<div class="alert alert-success" style="margin-bottom:12px;">✓ این فرصت ذخیره شد و در پیشنهادهای شما لحاظ میشود.</div>
|
||||||
}
|
}
|
||||||
<div class="aside-apply">
|
<div class="aside-apply">
|
||||||
<form method="post">
|
<button type="button" class="btn btn-accent btn-block btn-lg contact-trigger"
|
||||||
<button type="submit" asp-page-handler="Interest" asp-route-id="@s.Id"
|
data-contact-type="shift" data-contact-id="@s.Id">📞 اعلام تمایل و مشاهده راه ارتباطی</button>
|
||||||
class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده راه ارتباطی</button>
|
|
||||||
</form>
|
|
||||||
<p class="muted center" style="font-size:12px; margin:8px 0;">با اعلام تمایل، اطلاعات تماس مرکز نمایش داده میشود.</p>
|
<p class="muted center" style="font-size:12px; margin:8px 0;">با اعلام تمایل، اطلاعات تماس مرکز نمایش داده میشود.</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:8px;">
|
<div style="display:flex; gap:8px;">
|
||||||
@@ -196,26 +194,11 @@
|
|||||||
|
|
||||||
@* Sticky bottom action bar — mobile only. Always-reachable primary action (native-app feel). *@
|
@* Sticky bottom action bar — mobile only. Always-reachable primary action (native-app feel). *@
|
||||||
<div class="mobile-action-bar">
|
<div class="mobile-action-bar">
|
||||||
@if (Model.ShowContact)
|
<button type="button" class="btn btn-accent btn-lg cta-main contact-trigger"
|
||||||
{
|
data-contact-type="shift" data-contact-id="@s.Id">📞 اعلام تمایل و مشاهده تماس</button>
|
||||||
@if (!string.IsNullOrEmpty(f.Phone))
|
<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>
|
||||||
<a class="btn btn-accent btn-lg cta-main" href="tel:@f.Phone">📞 تماس با مرکز</a>
|
</form>
|
||||||
}
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(Model.MapKey) && Model.Shift?.Facility?.Lat is not null)
|
@if (!string.IsNullOrEmpty(Model.MapKey) && Model.Shift?.Facility?.Lat is not null)
|
||||||
|
|||||||
@@ -13,16 +13,6 @@
|
|||||||
comp = JalaliDate.Toman(pa) + " مدنظر";
|
comp = JalaliDate.Toman(pa) + " مدنظر";
|
||||||
else
|
else
|
||||||
comp = "توافقی";
|
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">
|
<div class="page-head">
|
||||||
@@ -67,41 +57,9 @@
|
|||||||
<aside>
|
<aside>
|
||||||
<div class="card card-pad">
|
<div class="card card-pad">
|
||||||
<h3 style="margin-top:0;">راههای ارتباطی</h3>
|
<h3 style="margin-top:0;">راههای ارتباطی</h3>
|
||||||
@{ var contacts = (t.Contacts ?? new List<JobsMedical.Web.Models.ContactMethod>()).OrderBy(c => c.SortOrder).ToList(); }
|
<button type="button" class="btn btn-accent btn-block btn-lg contact-trigger"
|
||||||
@if (contacts.Count > 0)
|
data-contact-type="talent" data-contact-id="@t.Id">📞 مشاهده راههای ارتباطی</button>
|
||||||
{
|
<p class="muted" style="font-size:12px; margin:10px 0 0;">با کلیک، شماره تماس و راههای ارتباطی نمایش داده میشود.</p>
|
||||||
<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>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -390,6 +390,62 @@ app.MapGet("/sitemap.xml", async (HttpContext ctx, AppDbContext db) =>
|
|||||||
return Results.Content(sb.ToString(), "application/xml");
|
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) ----
|
// ---- Instant search suggestions (typeahead dropdown) ----
|
||||||
app.MapGet("/search/suggest", async (string? q, AppDbContext db) =>
|
app.MapGet("/search/suggest", async (string? q, AppDbContext db) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -363,6 +363,22 @@ mark { background: #fff3bf; color: inherit; padding: 0 2px; border-radius: 3px;
|
|||||||
.contact-row .btn { flex: 0 0 auto; padding: 6px 14px; }
|
.contact-row .btn { flex: 0 0 auto; padding: 6px 14px; }
|
||||||
.badge-gender { background: #f3eefb; color: #6b3fa0; }
|
.badge-gender { background: #f3eefb; color: #6b3fa0; }
|
||||||
|
|
||||||
|
/* ---------- Contact modal (lazy-loaded numbers) ---------- */
|
||||||
|
.contact-modal { position: fixed; inset: 0; z-index: 200; display: none; align-items: center;
|
||||||
|
justify-content: center; padding: 16px; background: rgba(15,23,42,.55); }
|
||||||
|
.contact-modal.show { display: flex; animation: revealIn .2s ease; }
|
||||||
|
.contact-modal-box { background: var(--surface); border-radius: 16px; width: 100%; max-width: 420px;
|
||||||
|
box-shadow: 0 24px 60px rgba(0,0,0,.3); overflow: hidden; animation: revealIn .25s cubic-bezier(.2,.7,.3,1); }
|
||||||
|
.contact-modal-head { display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 14px 16px; border-bottom: 1px solid var(--line); }
|
||||||
|
.contact-modal-head h3 { margin: 0; font-size: 16px; }
|
||||||
|
.contact-modal-x { background: none; border: none; font-size: 18px; cursor: pointer; color: var(--muted);
|
||||||
|
line-height: 1; padding: 4px 6px; border-radius: 8px; }
|
||||||
|
.contact-modal-x:hover { background: var(--bg); color: var(--ink); }
|
||||||
|
.contact-modal-body { padding: 14px 16px; }
|
||||||
|
/* The card-level trigger sits inside an <a>; show it as the primary action. */
|
||||||
|
.contact-trigger { cursor: pointer; }
|
||||||
|
|
||||||
/* ---------- Filters layout ---------- */
|
/* ---------- Filters layout ---------- */
|
||||||
.layout-2 { display: grid; grid-template-columns: 270px 1fr; gap: 24px; align-items: start; }
|
.layout-2 { display: grid; grid-template-columns: 270px 1fr; gap: 24px; align-items: start; }
|
||||||
.filter-card { position: sticky; top: 84px; }
|
.filter-card { position: sticky; top: 84px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user