feat: area admin con editor tiptap, api crud e upload immagini

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 01:49:12 +02:00
parent f3fb2fe005
commit f0ad162f74
14 changed files with 423 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
---
import Admin from '../../../layouts/Admin.astro';
import PostForm from '../../../components/admin/PostForm.astro';
import { getDb } from '../../../lib/db';
import { getPostById } from '../../../lib/posts';
export const prerender = false;
const post = getPostById(getDb(), Number(Astro.params.id));
if (!post) return Astro.redirect('/admin');
---
<Admin title={`Modifica: ${post.title}`}>
<h1>Modifica articolo</h1>
<script id="initial-body" type="text/plain" set:html={post.body_html}></script>
<PostForm post={post} />
</Admin>
+37
View File
@@ -0,0 +1,37 @@
---
import Admin from '../../layouts/Admin.astro';
import { getDb } from '../../lib/db';
import { listAllPosts } from '../../lib/posts';
export const prerender = false;
const posts = listAllPosts(getDb());
const fmt = new Intl.DateTimeFormat('it-IT', { dateStyle: 'short', timeStyle: 'short' });
---
<Admin title="Articoli">
<h1>Articoli</h1>
<p><a class="abtn" href="/admin/new">+ Nuovo articolo</a></p>
<table>
<thead><tr><th>Titolo</th><th>Categoria</th><th>Stato</th><th>Aggiornato</th><th></th></tr></thead>
<tbody>
{posts.map((p) => (
<tr>
<td><a href={`/admin/edit/${p.id}`}>{p.title}</a></td>
<td>{p.category}</td>
<td><span class:list={['pill', { 'pill--draft': p.draft }]}>{p.draft ? 'Bozza' : 'Pubblicato'}</span></td>
<td>{fmt.format(new Date(p.updated_at.replace(' ', 'T') + 'Z'))}</td>
<td><button class="abtn abtn--danger" data-del={p.id}>Elimina</button></td>
</tr>
))}
</tbody>
</table>
{posts.length === 0 && <p>Nessun articolo: creane uno nuovo.</p>}
</Admin>
<script>
document.querySelectorAll<HTMLButtonElement>('[data-del]').forEach((b) =>
b.addEventListener('click', async () => {
if (!confirm('Eliminare definitivamente questo articolo?')) return;
const res = await fetch(`/api/admin/posts/${b.dataset.del}`, { method: 'DELETE' });
if (res.ok) location.reload();
else alert('Eliminazione non riuscita.');
}));
</script>
+34
View File
@@ -0,0 +1,34 @@
---
import Admin from '../../layouts/Admin.astro';
import { getDb } from '../../lib/db';
import { login, SESSION_COOKIE } from '../../lib/auth';
import { rateLimit } from '../../lib/rate-limit';
export const prerender = false;
let error = '';
if (Astro.request.method === 'POST') {
if (!rateLimit(`login:${Astro.clientAddress}`, 5, 15 * 60 * 1000)) {
error = 'Troppi tentativi: riprova tra 15 minuti.';
} else {
const form = await Astro.request.formData();
const token = login(getDb(), String(form.get('username') ?? ''), String(form.get('password') ?? ''));
if (token) {
Astro.cookies.set(SESSION_COOKIE, token, {
httpOnly: true, sameSite: 'lax', path: '/',
secure: import.meta.env.PROD, maxAge: 7 * 24 * 3600,
});
return Astro.redirect('/admin');
}
error = 'Credenziali non valide.';
}
}
---
<Admin title="Login">
<h1>Accesso</h1>
{error && <p style="color:#a33">{error}</p>}
<form method="post" style="max-width:360px">
<input class="afield" name="username" placeholder="Utente" required autocomplete="username" />
<input class="afield" type="password" name="password" placeholder="Password" required autocomplete="current-password" />
<button class="abtn" type="submit">Entra</button>
</form>
</Admin>
+11
View File
@@ -0,0 +1,11 @@
import type { APIRoute } from 'astro';
import { getDb } from '../../lib/db';
import { logout, SESSION_COOKIE } from '../../lib/auth';
export const prerender = false;
export const GET: APIRoute = ({ cookies, redirect }) => {
const token = cookies.get(SESSION_COOKIE)?.value;
if (token) logout(getDb(), token);
cookies.delete(SESSION_COOKIE, { path: '/' });
return redirect('/admin/login');
};
+10
View File
@@ -0,0 +1,10 @@
---
import Admin from '../../layouts/Admin.astro';
import PostForm from '../../components/admin/PostForm.astro';
export const prerender = false;
---
<Admin title="Nuovo articolo">
<h1>Nuovo articolo</h1>
<script id="initial-body" type="text/plain"></script>
<PostForm />
</Admin>