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
+29
View File
@@ -0,0 +1,29 @@
import type { APIRoute } from 'astro';
import { getDb } from '../../../../lib/db';
import { getPostById, updatePost, deletePost } from '../../../../lib/posts';
import { parsePostBody } from '../../../../lib/post-body';
export const prerender = false;
const json = (status: number, body: object) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
export const PUT: APIRoute = async ({ params, request }) => {
const id = Number(params.id);
if (!getPostById(getDb(), id)) return json(404, { error: 'Articolo non trovato.' });
let data: Record<string, unknown>;
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
const parsed = parsePostBody(data);
if ('error' in parsed) return json(400, { error: parsed.error });
try {
updatePost(getDb(), id, parsed.value);
return json(200, { ok: true });
} catch (err: any) {
if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
throw err;
}
};
export const DELETE: APIRoute = ({ params }) => {
deletePost(getDb(), Number(params.id));
return json(200, { ok: true });
};
+22
View File
@@ -0,0 +1,22 @@
import type { APIRoute } from 'astro';
import { getDb } from '../../../../lib/db';
import { createPost } from '../../../../lib/posts';
import { parsePostBody } from '../../../../lib/post-body';
export const prerender = false;
const json = (status: number, body: object) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
export const POST: APIRoute = async ({ request }) => {
let data: Record<string, unknown>;
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
const parsed = parsePostBody(data);
if ('error' in parsed) return json(400, { error: parsed.error });
try {
const id = createPost(getDb(), parsed.value);
return json(201, { id, slug: parsed.value.slug });
} catch (err: any) {
if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
throw err;
}
};
+26
View File
@@ -0,0 +1,26 @@
import type { APIRoute } from 'astro';
import { writeFile, mkdir } from 'node:fs/promises';
import { randomBytes } from 'node:crypto';
import { join, extname } from 'node:path';
import { getEnv } from '../../../lib/env';
export const prerender = false;
const ALLOWED: Record<string, string> = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' };
const MAX_BYTES = 5 * 1024 * 1024;
const json = (status: number, body: object) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
export const POST: APIRoute = async ({ request }) => {
const form = await request.formData().catch(() => null);
const file = form?.get('file');
if (!(file instanceof File)) return json(400, { error: 'Nessun file.' });
const ext = extname(file.name).toLowerCase();
if (!ALLOWED[ext]) return json(400, { error: 'Formato non consentito (jpg, png, webp, gif).' });
if (file.size > MAX_BYTES) return json(400, { error: 'File troppo grande (max 5 MB).' });
const dir = getEnv('UPLOADS_DIR', 'uploads');
await mkdir(dir, { recursive: true });
const name = `${Date.now()}-${randomBytes(4).toString('hex')}${ext}`;
await writeFile(join(dir, name), Buffer.from(await file.arrayBuffer()));
return json(200, { url: `/uploads/${name}` });
};