Files
aia-website/www/src/components/LikeViewCounter.astro
T
DeCentN2Madness a178e05f7b feat(library): restore live-site intro copy above post listings
- add the two live intro paragraphs between the hero and article grid
- keep the library listing cards in the live two-column layout
- restore the 4:3 cropped article thumbnails and card spacing via library-scoped selectors
- avoid leaking the card-grid overrides into unrelated pages
- add likes/views from api to library article cards
2026-06-11 19:08:24 -07:00

123 lines
4.7 KiB
Plaintext

---
const { slug } = Astro.props;
---
<div class="like-view-counter" data-counter>
<span aria-label="Article views"><span class="view-count" data-views>0</span> views</span>
<span class="like-group">
<span class="like-count" data-likes>—</span>
<button class="like-button" type="button" data-like data-slug={slug} aria-label="Like this article" aria-pressed="false">
<svg aria-hidden="true" viewBox="0 0 12 10" width="20" height="20" focusable="false" class="like-icon">
<path d="m1 4.4c-1-4 2.5-5 4.5-2.5l.5 .6l.5-.6c2-2.5 6-1.5 4.5 2.5q-1 3-5 5q-4-2-5-5z"></path>
</svg>
</button>
</span>
</div>
<script>
const apiBase = import.meta.env.PUBLIC_AIA_API_BASE || 'https://api.azinstitute4autism.com';
const cleanSlug = (raw: string | undefined) =>
raw ? raw.split('?')[0].replace(/^\/+|\/+$/g, '') : '';
const readCookies = () =>
Object.fromEntries(
document.cookie
.split('; ')
.filter(Boolean)
.map((pair) => pair.split('=').map(decodeURIComponent)),
);
const setLikedCookie = (name: string, liked: boolean) => {
if (!liked) {
document.cookie = `${encodeURIComponent(name)}=; Max-Age=0; path=/`;
return;
}
const expires = new Date(Date.now() + 365 * 864e5).toUTCString();
document.cookie = `${encodeURIComponent(name)}=1; expires=${expires}; path=/`;
};
const fetchJson = async (url: string, options: RequestInit) => {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
};
document.querySelectorAll<HTMLElement>('[data-counter]').forEach(async (counter) => {
const button = counter.querySelector<HTMLButtonElement>('[data-like]');
const slug = cleanSlug(button?.dataset.slug);
const likes = counter.querySelector<HTMLElement>('[data-likes]');
const views = counter.querySelector<HTMLElement>('[data-views]');
if (!button || !slug) return;
const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(window.location.hostname);
if (isLocalhost) {
counter.dataset.unavailable = 'true';
if (likes) likes.textContent = '0';
if (views) views.textContent = '0';
return;
}
const cookieName = `liked_${slug}`;
const renderLiked = (liked: boolean) => {
button.classList.toggle('liked', liked);
button.setAttribute('aria-pressed', String(liked));
};
renderLiked(Object.prototype.hasOwnProperty.call(readCookies(), cookieName));
try {
const data = await fetchJson(`${apiBase}/stats/batch`, {
method: 'POST',
credentials: 'include',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slugs: [slug] }),
});
const stats = data[slug];
if (likes) likes.textContent = String(stats?.likes ?? 0);
if (views) views.textContent = String(stats?.views ?? 0);
} catch {
counter.dataset.unavailable = 'true';
if (likes) likes.textContent = '0';
if (views) views.textContent = '0';
}
button.addEventListener('click', async () => {
const willLike = !button.classList.contains('liked');
button.disabled = true;
try {
const data = await fetchJson(
willLike ? `${apiBase}/likes` : `${apiBase}/likes/${encodeURIComponent(slug)}`,
willLike
? {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug }),
}
: { method: 'DELETE', credentials: 'include' },
);
setLikedCookie(cookieName, willLike);
renderLiked(willLike);
if (likes) likes.textContent = String(data.count ?? 0);
} catch {
counter.dataset.unavailable = 'true';
} finally {
button.disabled = false;
}
});
});
</script>
<style>
.like-view-counter { align-items: center; border-top: 1px solid var(--color-border); display: flex; gap: var(--space-lg); font-size: .75rem; margin-block: var(--space-xl); padding-top: var(--space-md); }
.like-view-counter { font-size: .8rem; }
.like-group { align-items: center; display: inline-flex; gap: .35rem; }
.like-view-counter button { align-items: center; background: white; border: none; color: inherit; display: inline-flex; gap: 0; padding: 0; }
.like-view-counter .like-count { color: var(--color-text); min-width: 1ch; }
.like-view-counter .like-icon { fill: none; flex: 0 0 auto; stroke: #d92d20; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
.like-view-counter button.liked .like-icon { fill: #d92d20; stroke: #d92d20; }
.like-view-counter button:disabled { cursor: wait; opacity: .65; }
</style>