6a97a98325
Eliminare un articolo non lasciava nulla di recuperabile: ora la riga finisce nel cestino (colonna deleted_at, aggiunta anche ai database già esistenti) e sparisce da sito e pannello, ma resta ripristinabile dalla nuova pagina /admin/cestino, dove sta anche la cancellazione definitiva. L'API la accetta solo su articoli già cestinati, così il contenuto non si perde per un clic distratto. Ogni lettura pubblica — elenco, recenti, archivio per mese, pagina del singolo articolo — filtra il cestino, e chi non gestisce vede nel cestino soltanto i propri articoli. Le azioni di riga diventano icone (matita e cestino) come nell'elenco utenti, con etichetta accessibile che nomina l'articolo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
64 lines
2.9 KiB
Plaintext
64 lines
2.9 KiB
Plaintext
---
|
|
import Admin from '../../layouts/Admin.astro';
|
|
import { getDb } from '../../lib/db';
|
|
import { listAllPosts, listByAuthor, listTrashed } from '../../lib/posts';
|
|
import { listUsers } from '../../lib/auth';
|
|
export const prerender = false;
|
|
const user = Astro.locals.user!;
|
|
const isManager = user.role === 'superuser' || user.role === 'admin';
|
|
const posts = isManager ? listAllPosts(getDb()) : listByAuthor(getDb(), user.id);
|
|
const cestinati = listTrashed(getDb(), isManager ? undefined : user.id).length;
|
|
const nameById = new Map(listUsers(getDb()).map((u) => [u.id, u.username]));
|
|
const fmt = new Intl.DateTimeFormat('it-IT', { dateStyle: 'short', timeStyle: 'short' });
|
|
---
|
|
<Admin title="Articoli">
|
|
<h1>Articoli</h1>
|
|
|
|
<p class="atoolbar">
|
|
<a class="abtn" href="/admin/new">+ Nuovo articolo</a>
|
|
{cestinati > 0 && <a class="abtn abtn--ghost" href="/admin/cestino">Cestino ({cestinati})</a>}
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Titolo</th><th>Categoria</th>{isManager && <th>Autore</th>}<th>Stato</th><th>Aggiornato</th><th class="uactions">Azioni</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{posts.map((p) => (
|
|
<tr>
|
|
<td><a href={`/admin/edit/${p.id}`}>{p.title}</a></td>
|
|
<td>{p.category}</td>
|
|
{isManager && <td>{p.author_id ? (nameById.get(p.author_id) ?? 'Staff InsanityLab') : 'Staff InsanityLab'}</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 class="uactions">
|
|
<a class="aicon" href={`/admin/edit/${p.id}`} title="Modifica" aria-label={`Modifica ${p.title}`}>
|
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m4 17 9.5-9.5 3 3L7 20H4v-3Zm11.7-11.7 1.6-1.6a1.4 1.4 0 0 1 2 0l1 1a1.4 1.4 0 0 1 0 2l-1.6 1.6-3-3Z"/></svg>
|
|
</a>
|
|
<button class="aicon aicon--danger" data-del={p.id} title="Sposta nel cestino" aria-label={`Sposta ${p.title} nel cestino`}>
|
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3h6l1 2h4v2H4V5h4l1-2Zm-3 6h12l-1 11a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L6 9Z"/></svg>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{posts.length === 0 && <p class="amuted">Nessun articolo: creane uno nuovo.</p>}
|
|
<p class="form-msg" id="row-msg"></p>
|
|
</Admin>
|
|
|
|
<script>
|
|
const rowMsg = document.getElementById('row-msg')!;
|
|
|
|
document.querySelectorAll<HTMLButtonElement>('[data-del]').forEach((b) =>
|
|
b.addEventListener('click', async () => {
|
|
if (!confirm('Spostare questo articolo nel cestino? Sparirà dal sito, ma potrai recuperarlo.')) return;
|
|
const res = await fetch(`/api/admin/posts/${b.dataset.del}`, { method: 'DELETE' });
|
|
if (res.ok) location.reload();
|
|
else {
|
|
rowMsg.className = 'form-msg form-msg--err';
|
|
rowMsg.textContent = (await res.json()).error ?? 'Operazione non riuscita.';
|
|
}
|
|
}));
|
|
</script>
|