(function () {
const meta = document.querySelector('meta[name="csrf-token"]');
const csrfToken = meta ? meta.content : '';
// ── Liker tooltip on hover ────────────────────────────────────────────────
const cache = {};
let activeTooltip = null;
let hideTimer = null;
function removeTooltip() {
if (activeTooltip) { activeTooltip.remove(); activeTooltip = null; }
}
function showTooltip(btn, data) {
removeTooltip();
const action = btn.dataset.socialAction;
const names = action === 'like' ? data.likers : data.favoriters;
if (!names || names.length === 0) return;
const tip = document.createElement('div');
tip.className = 'social-tooltip';
const label = action === 'like' ? 'Liked by' : 'Favorited by';
const ul = names.map(n => `
${n.replace(/`).join('');
tip.innerHTML = `${label}`;
// Attach to body so overflow:hidden on ancestor cards can't clip it.
document.body.appendChild(tip);
activeTooltip = tip;
const rect = btn.getBoundingClientRect();
tip.style.position = 'fixed';
tip.style.top = (rect.bottom + 6) + 'px';
tip.style.left = rect.left + 'px';
requestAnimationFrame(() => tip.classList.add('visible'));
}
async function fetchLikers(type, id) {
const key = `${type}:${id}`;
if (cache[key]) return cache[key];
const res = await fetch(`/api/likers/${type}/${id}`);
if (!res.ok) return null;
const data = await res.json();
cache[key] = data;
return data;
}
function invalidateCache(type, id) {
delete cache[`${type}:${id}`];
}
document.addEventListener('mouseenter', async (e) => {
const btn = e.target.closest('[data-social-action]');
if (!btn || btn.hasAttribute('data-require-login')) return;
const countEl = btn.querySelector('.social-count');
if (!countEl || Number(countEl.textContent) === 0) return;
clearTimeout(hideTimer);
const { socialType, socialId } = btn.dataset;
const data = await fetchLikers(socialType, socialId);
if (data) showTooltip(btn, data);
}, true);
document.addEventListener('mouseleave', (e) => {
const btn = e.target.closest('[data-social-action]');
if (!btn) return;
hideTimer = setTimeout(removeTooltip, 100);
}, true);
document.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-social-action]');
if (!btn) return;
// Guest: redirect to login
if (btn.hasAttribute('data-require-login')) {
window.location.href = '/login';
return;
}
// Prevent card from navigating when button is inside one
e.stopPropagation();
const { socialAction, socialType, socialId } = btn.dataset;
const url = `/api/${socialType}s/${socialId}/${socialAction}`;
btn.disabled = true;
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const isOn = data.liked ?? data.favorited;
btn.classList.toggle('active', isOn);
btn.setAttribute('aria-pressed', String(isOn));
const countEl = btn.querySelector('.social-count');
if (countEl) countEl.textContent = data.count;
invalidateCache(btn.dataset.socialType, btn.dataset.socialId);
} catch (err) {
console.error('[social] toggle failed:', err);
} finally {
btn.disabled = false;
}
});
})();