d8862ba8ec
I filtri non dicevano dove ci fosse materiale: si sceglieva una categoria per scoprirla vuota. Ora ognuna porta il proprio conteggio, cestino e bozze esclusi, e il numero si aggiorna insieme ai filtri quando si cambia tematica senza ricaricare. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
7.2 KiB
Plaintext
149 lines
7.2 KiB
Plaintext
---
|
|
import Base from '../../layouts/Base.astro';
|
|
import PageHero from '../../components/PageHero.astro';
|
|
import { getDb } from '../../lib/db';
|
|
import { listPublished, countPublishedByCategory } from '../../lib/posts';
|
|
import { CATEGORIES } from '../../lib/blog-const';
|
|
export const prerender = false;
|
|
|
|
const PER_PAGE = 9;
|
|
const url = Astro.url;
|
|
const category = url.searchParams.get('categoria') ?? undefined;
|
|
const page = Math.max(1, Number(url.searchParams.get('pagina') ?? '1') || 1);
|
|
const { items, total } = listPublished(getDb(), {
|
|
category: category && (CATEGORIES as readonly string[]).includes(category) ? category : undefined,
|
|
page, perPage: PER_PAGE,
|
|
});
|
|
const pages = Math.max(1, Math.ceil(total / PER_PAGE));
|
|
// Conteggi per categoria: mostrati accanto ai filtri, così si vede subito dove c'è materiale.
|
|
const perCategoria = countPublishedByCategory(getDb());
|
|
const totalePubblicati = Object.values(perCategoria).reduce((n, v) => n + v, 0);
|
|
const fmt = (d: string | null) => d ? new Intl.DateTimeFormat('it-IT', { dateStyle: 'long' }).format(new Date(d)) : '';
|
|
const linkFor = (cat?: string, p = 1) => {
|
|
const q = new URLSearchParams();
|
|
if (cat) q.set('categoria', cat);
|
|
if (p > 1) q.set('pagina', String(p));
|
|
const s = q.toString();
|
|
return `/blog${s ? `?${s}` : ''}`;
|
|
};
|
|
---
|
|
<Base title="Blog & Edugo" description="Edugo è lo spazio di InsanityLab dedicato alla conoscenza: articoli, guide e approfondimenti su allenamento, nutrizione e salute.">
|
|
<PageHero title="Blog & Edugo" />
|
|
<section class="section">
|
|
<div class="container">
|
|
<div class="section-heading">
|
|
<h2>Blog & Edugo</h2>
|
|
<p>Edugo è lo spazio di InsanityLab dedicato alla conoscenza: articoli, guide e approfondimenti su allenamento, nutrizione e salute per capire il corpo e migliorare il proprio benessere. Perché la conoscenza è il primo passo del cambiamento.</p>
|
|
</div>
|
|
<nav class="bfilters" aria-label="categorie">
|
|
<a class:list={['bfilters__item', { 'is-active': !category }]} href={linkFor()}>
|
|
Mostra tutto <span class="bfilters__n">{totalePubblicati}</span>
|
|
</a>
|
|
{CATEGORIES.map((c) => (
|
|
<a class:list={['bfilters__item', { 'is-active': category === c }]} href={linkFor(c)}>
|
|
{c} <span class="bfilters__n">{perCategoria[c] ?? 0}</span>
|
|
</a>
|
|
))}
|
|
</nav>
|
|
<div id="blog-results">
|
|
{items.length === 0 ? (
|
|
<p class="bempty">Nessun articolo pubblicato{category ? ' in questa categoria' : ''}, torna a trovarci presto.</p>
|
|
) : (
|
|
<div class="bgrid">
|
|
{items.map((post) => (
|
|
<article class="bcard">
|
|
{post.cover && <a href={`/blog/${post.slug}`}><img src={post.cover} alt="" loading="lazy" /></a>}
|
|
<p class="bcard__date">{fmt(post.published_at)}</p>
|
|
<h3><a href={`/blog/${post.slug}`}>{post.title}</a></h3>
|
|
<p>{post.excerpt}</p>
|
|
<a class="bcard__more" href={`/blog/${post.slug}`}>Per saperne di più →</a>
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
{pages > 1 && (
|
|
<nav class="bpages" aria-label="paginazione">
|
|
{Array.from({ length: pages }, (_, i) => (
|
|
<a class:list={['bpages__n', { 'is-active': page === i + 1 }]} href={linkFor(category, i + 1)}>{i + 1}</a>
|
|
))}
|
|
</nav>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</Base>
|
|
|
|
<script>
|
|
// Cambio categoria e pagina senza ricaricare: si scarica la stessa pagina e si sostituiscono
|
|
// solo la griglia e i filtri. I link restano normali, quindi senza JS tutto continua a
|
|
// funzionare come prima.
|
|
const risultati = document.getElementById('blog-results');
|
|
const filtri = document.querySelector('.bfilters');
|
|
|
|
if (risultati && filtri) {
|
|
let inCorso: AbortController | null = null;
|
|
|
|
async function mostra(href: string, scorri: boolean): Promise<void> {
|
|
inCorso?.abort();
|
|
const controller = new AbortController();
|
|
inCorso = controller;
|
|
risultati!.classList.add('is-loading');
|
|
try {
|
|
const res = await fetch(href, { signal: controller.signal });
|
|
const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
|
|
const nuoviRisultati = doc.getElementById('blog-results');
|
|
const nuoviFiltri = doc.querySelector('.bfilters');
|
|
// Se la pagina non ha la forma attesa (errore, redirect) si lascia fare al browser.
|
|
if (!nuoviRisultati || !nuoviFiltri) { location.href = href; return; }
|
|
risultati!.innerHTML = nuoviRisultati.innerHTML;
|
|
filtri!.innerHTML = nuoviFiltri.innerHTML;
|
|
if (scorri) filtri!.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
} catch (e) {
|
|
if ((e as Error).name === 'AbortError') return;
|
|
location.href = href;
|
|
return;
|
|
} finally {
|
|
if (inCorso === controller) {
|
|
risultati!.classList.remove('is-loading');
|
|
inCorso = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delega sul documento: i link vengono rigenerati a ogni cambio, così non serve riagganciarli.
|
|
document.addEventListener('click', (e) => {
|
|
const link = (e.target as Element).closest<HTMLAnchorElement>('.bfilters__item, .bpages__n');
|
|
if (!link || e.metaKey || e.ctrlKey || e.shiftKey || (e as MouseEvent).button !== 0) return;
|
|
e.preventDefault();
|
|
if (link.classList.contains('is-active')) return;
|
|
history.pushState({}, '', link.href);
|
|
mostra(link.href, link.classList.contains('bpages__n'));
|
|
});
|
|
|
|
// Avanti/indietro del browser: si ricarica il contenuto senza toccare la cronologia.
|
|
addEventListener('popstate', () => mostra(location.href, false));
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.bfilters { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-bottom: 50px; }
|
|
.bfilters__item { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; text-decoration: none; color: var(--c-text); padding: 10px 16px; }
|
|
.bfilters__item.is-active { background: var(--c-accent); color: #fff; }
|
|
.bfilters__n { color: var(--c-text-light); }
|
|
.bfilters__item.is-active .bfilters__n { color: rgba(255,255,255,.75); }
|
|
.bfilters__item { transition: background .2s ease, color .2s ease; }
|
|
#blog-results { transition: opacity .25s ease; }
|
|
#blog-results.is-loading { opacity: .35; }
|
|
.bempty { text-align: center; }
|
|
.bgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 50px 40px; }
|
|
.bcard img { aspect-ratio: 4/3; object-fit: cover; width: 100%; margin-bottom: 14px; }
|
|
.bcard__date { font-size: .78rem; color: var(--c-text-light); margin-bottom: 4px; }
|
|
.bcard h3 a { text-decoration: none; }
|
|
.bcard__more { font-family: var(--font-heading); font-size: .7rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
|
|
.bpages { display: flex; gap: 8px; justify-content: center; margin-top: 50px; }
|
|
.bpages__n { font-family: var(--font-heading); text-decoration: none; padding: 8px 14px; color: var(--c-text); }
|
|
.bpages__n.is-active { background: var(--c-dark); color: #fff; }
|
|
@media (max-width: 991px) { .bgrid { grid-template-columns: 1fr 1fr; } }
|
|
@media (max-width: 600px) { .bgrid { grid-template-columns: 1fr; } }
|
|
</style>
|