(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; } }); })();