Files
InsanityLab-web/.superpowers/sdd/review-e984010..f3fb2fe.diff
Adriano b913a2bf90 registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:36:26 +02:00

184 lines
7.9 KiB
Diff

# Review package: e984010..HEAD
## Commits
f3fb2fe feat: blog pubblico con filtri, paginazione e articolo
## Files changed
src/components/blog/Sidebar.astro | 26 +++++++++++++
src/lib/blog-const.ts | 6 +++
src/pages/blog/[slug].astro | 33 ++++++++++++++++
src/pages/blog/index.astro | 81 +++++++++++++++++++++++++++++++++++++++
4 files changed, 146 insertions(+)
## Diff
diff --git a/src/components/blog/Sidebar.astro b/src/components/blog/Sidebar.astro
new file mode 100644
index 0000000..a89dcc5
--- /dev/null
+++ b/src/components/blog/Sidebar.astro
@@ -0,0 +1,26 @@
+---
+import { getDb } from '../../lib/db';
+import { listRecent, listArchiveMonths } from '../../lib/posts';
+import { CATEGORIES } from '../../lib/blog-const';
+const db = getDb();
+const recent = listRecent(db, 4);
+const months = listArchiveMonths(db);
+const monthLabel = (m: string) => new Intl.DateTimeFormat('it-IT', { month: 'long', year: 'numeric' }).format(new Date(`${m}-01`));
+---
+<aside class="bside">
+ <h3>Categorie</h3>
+ {CATEGORIES.map((c) => <p><a href={`/blog?categoria=${encodeURIComponent(c)}`}>{c}</a></p>)}
+ <h3>Ultimi articoli</h3>
+ {recent.map((p) => (
+ <p><a href={`/blog/${p.slug}`}>{p.title}</a></p>
+ ))}
+ <h3>Archivio</h3>
+ {months.map((m) => <p class="bside__month">{monthLabel(m.month)} ({m.count})</p>)}
+</aside>
+
+<style>
+ .bside h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; margin-top: 30px; }
+ .bside a { text-decoration: none; color: var(--c-text); }
+ .bside a:hover { color: var(--c-accent-dark); }
+ .bside__month { text-transform: capitalize; }
+</style>
diff --git a/src/lib/blog-const.ts b/src/lib/blog-const.ts
new file mode 100644
index 0000000..d1b7436
--- /dev/null
+++ b/src/lib/blog-const.ts
@@ -0,0 +1,6 @@
+export const CATEGORIES = [
+ 'Articoli tecnici & scientifici',
+ 'Contenuti educativi',
+ 'LinkedIn / Insight',
+ 'Eventi & Presidi',
+] as const;
diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro
new file mode 100644
index 0000000..fdf30f1
--- /dev/null
+++ b/src/pages/blog/[slug].astro
@@ -0,0 +1,33 @@
+---
+import Base from '../../layouts/Base.astro';
+import Sidebar from '../../components/blog/Sidebar.astro';
+import { getDb } from '../../lib/db';
+import { getPublishedBySlug } from '../../lib/posts';
+export const prerender = false;
+
+const post = getPublishedBySlug(getDb(), Astro.params.slug!);
+if (!post) return Astro.redirect('/404');
+const fmt = new Intl.DateTimeFormat('it-IT', { dateStyle: 'long' });
+---
+<Base title={post.title} description={post.excerpt}>
+ <section class="section">
+ <div class="container art">
+ <article>
+ {post.cover && <img class="art__cover" src={post.cover} alt="" />}
+ <p class="art__meta">{post.published_at && fmt.format(new Date(post.published_at))} · {post.category}</p>
+ <h1>{post.title}</h1>
+ <div class="art__body" set:html={post.body_html} />
+ </article>
+ <Sidebar />
+ </div>
+ </section>
+</Base>
+
+<style>
+ .art { display: grid; grid-template-columns: 2.4fr 1fr; gap: 60px; }
+ .art__cover { width: 100%; margin-bottom: 24px; }
+ .art__meta { font-size: .8rem; color: var(--c-text-light); }
+ .art__body :global(img) { margin-block: 20px; }
+ .art__body :global(blockquote) { border-left: 3px solid var(--c-accent); margin: 24px 0; padding: 6px 0 6px 20px; font-style: italic; color: var(--c-heading); }
+ @media (max-width: 991px) { .art { grid-template-columns: 1fr; } }
+</style>
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
new file mode 100644
index 0000000..dcd8a05
--- /dev/null
+++ b/src/pages/blog/index.astro
@@ -0,0 +1,81 @@
+---
+import Base from '../../layouts/Base.astro';
+import PageHero from '../../components/PageHero.astro';
+import { getDb } from '../../lib/db';
+import { listPublished } 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));
+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</a>
+ {CATEGORIES.map((c) => (
+ <a class:list={['bfilters__item', { 'is-active': category === c }]} href={linkFor(c)}>{c}</a>
+ ))}
+ </nav>
+ {items.length === 0 ? (
+ <p style="text-align:center">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>
+ </section>
+</Base>
+
+<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; }
+ .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>