Add per-user Like (پسندیدن) with a liked page and counts
CI/CD / CI · dotnet build (push) Successful in 2m54s
CI/CD / Deploy · hamkadr (push) Successful in 2m48s

Logged-in users can like a listing (job/shift/talent); dislike is removed per request — only likes.
- Like model (polymorphic by TargetType+TargetId) + EF migration; unique per (user, listing).
- POST /like toggles the like (auth required) and returns {liked, count}.
- Detail pages: the old ♡ Save / ✕ Dismiss buttons are replaced by a single heart Like button that
  shows the live count and toggles in place; clicking while logged out redirects to login.
- New «❤️ پسندیده‌ها» page (/Me/Liked) lists everything the user liked (open listings only), with a
  nav entry shown only when authenticated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-23 12:25:10 +03:30
parent 39c866f4c7
commit c1c914df9f
16 changed files with 2002 additions and 24 deletions
@@ -112,6 +112,10 @@
<nav class="main-nav">
<a asp-page="/Index" class="@(path == "/" ? "active" : null)">خانه</a>
<a asp-page="/Recommendations/Index" class="@(path.StartsWith("/Recommendations") ? "active" : null)">✨ پیشنهادها</a>
@if (User.Identity?.IsAuthenticated == true)
{
<a asp-page="/Me/Liked" class="@(path.StartsWith("/Me/Liked") ? "active" : null)">❤️ پسندیده‌ها</a>
}
<a href="/Shifts" data-tour="shifts" class="@(path.StartsWith("/Shifts") ? "active" : null)">شیفت‌ها</a>
<a href="/Jobs" data-tour="jobs" class="@(path.StartsWith("/Jobs") ? "active" : null)">استخدام</a>
<a asp-page="/Talent/Index" class="@(path.StartsWith("/Talent") ? "active" : null)">آماده به کار</a>
@@ -290,6 +294,36 @@
<div id="contactModalBody" class="contact-modal-body"></div>
</div>
</div>
@* Like («پسندیدن») toggle — login-gated, updates the button state + count in place. *@
<script>
(function () {
function fa(n) { return String(n).replace(/[0-9]/g, function (d) { return '۰۱۲۳۴۵۶۷۸۹'[+d]; }); }
document.addEventListener('click', function (e) {
var b = e.target.closest('.like-trigger');
if (!b) return;
e.preventDefault();
if (document.body.dataset.authed !== '1') {
location.href = '/Account/Login?returnUrl=' + encodeURIComponent(location.pathname);
return;
}
var fd = new FormData();
fd.append('type', b.dataset.likeType);
fd.append('id', b.dataset.likeId);
b.disabled = true;
fetch('/like', { method: 'POST', body: fd })
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
.then(function (d) {
b.dataset.liked = d.liked ? 'true' : 'false';
b.classList.toggle('btn-accent', d.liked);
b.classList.toggle('btn-outline', !d.liked);
var ico = b.querySelector('.like-ico'); if (ico) ico.textContent = d.liked ? '♥' : '♡';
var c = b.querySelector('.like-count'); if (c) c.textContent = fa(d.count);
})
.catch(function () {})
.finally(function () { b.disabled = false; });
});
})();
</script>
<script>
(function () {
var modal = document.getElementById('contactModal');