f0ad162f74
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
5.8 KiB
Plaintext
137 lines
5.8 KiB
Plaintext
---
|
|
import { CATEGORIES } from '../../lib/blog-const';
|
|
import type { Post } from '../../lib/posts';
|
|
interface Props { post?: Post }
|
|
const { post } = Astro.props;
|
|
---
|
|
<form id="post-form" data-id={post?.id ?? ''}>
|
|
<input class="afield" name="title" placeholder="Titolo" required value={post?.title ?? ''} />
|
|
<input class="afield" name="slug" placeholder="Slug (vuoto = generato dal titolo)" value={post?.slug ?? ''} />
|
|
<select class="afield" name="category">
|
|
{CATEGORIES.map((c) => <option value={c} selected={post?.category === c}>{c}</option>)}
|
|
</select>
|
|
<textarea class="afield" name="excerpt" placeholder="Estratto (anteprima in lista blog)" maxlength="300">{post?.excerpt ?? ''}</textarea>
|
|
|
|
<label>Copertina: <input type="file" id="cover-file" accept="image/*" /></label>
|
|
<input type="hidden" name="cover" value={post?.cover ?? ''} />
|
|
<p id="cover-preview">{post?.cover && <img src={post.cover} style="max-width:200px" />}</p>
|
|
|
|
<div class="ed-toolbar" id="ed-toolbar">
|
|
<button type="button" data-cmd="h2">H2</button>
|
|
<button type="button" data-cmd="h3">H3</button>
|
|
<button type="button" data-cmd="bold"><b>B</b></button>
|
|
<button type="button" data-cmd="italic"><i>I</i></button>
|
|
<button type="button" data-cmd="bullet">• Elenco</button>
|
|
<button type="button" data-cmd="ordered">1. Elenco</button>
|
|
<button type="button" data-cmd="quote">" Citazione</button>
|
|
<button type="button" data-cmd="link">Link</button>
|
|
<button type="button" data-cmd="image">Immagine</button>
|
|
</div>
|
|
<div id="editor" class="ed-body"></div>
|
|
|
|
<label><input type="checkbox" name="draft" checked={post ? post.draft === 1 : true} /> Bozza (non visibile sul sito)</label>
|
|
<p><button class="abtn" type="submit">Salva</button> <span id="save-msg"></span></p>
|
|
</form>
|
|
|
|
<style>
|
|
.ed-toolbar { display: flex; gap: 6px; flex-wrap: wrap; background: #fff; border: 1px solid #ccc; border-bottom: 0; padding: 8px; }
|
|
.ed-toolbar button { background: #eee; border: 1px solid #ccc; padding: 4px 10px; cursor: pointer; }
|
|
.ed-toolbar button.is-active { background: #b5a48b; color: #fff; }
|
|
.ed-body { background: #fff; border: 1px solid #ccc; min-height: 340px; padding: 12px 16px; margin-bottom: 14px; }
|
|
.ed-body :global(.ProseMirror) { outline: none; min-height: 320px; }
|
|
</style>
|
|
|
|
<script>
|
|
import { Editor } from '@tiptap/core';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import Image from '@tiptap/extension-image';
|
|
import Link from '@tiptap/extension-link';
|
|
|
|
const form = document.getElementById('post-form') as HTMLFormElement;
|
|
const initial = (document.getElementById('initial-body') as HTMLScriptElement | null)?.textContent ?? '';
|
|
|
|
const editor = new Editor({
|
|
element: document.getElementById('editor')!,
|
|
extensions: [
|
|
StarterKit.configure({ heading: { levels: [2, 3] } }),
|
|
Image,
|
|
Link.configure({ openOnClick: false }),
|
|
],
|
|
content: initial,
|
|
});
|
|
|
|
async function uploadFile(file: File): Promise<string | null> {
|
|
const fd = new FormData();
|
|
fd.append('file', file);
|
|
const res = await fetch('/api/admin/upload', { method: 'POST', body: fd });
|
|
if (!res.ok) { alert('Upload non riuscito.'); return null; }
|
|
return (await res.json()).url;
|
|
}
|
|
|
|
document.getElementById('ed-toolbar')!.addEventListener('click', async (e) => {
|
|
const btn = (e.target as HTMLElement).closest('button');
|
|
if (!btn) return;
|
|
const c = editor.chain().focus();
|
|
switch (btn.dataset.cmd) {
|
|
case 'h2': c.toggleHeading({ level: 2 }).run(); break;
|
|
case 'h3': c.toggleHeading({ level: 3 }).run(); break;
|
|
case 'bold': c.toggleBold().run(); break;
|
|
case 'italic': c.toggleItalic().run(); break;
|
|
case 'bullet': c.toggleBulletList().run(); break;
|
|
case 'ordered': c.toggleOrderedList().run(); break;
|
|
case 'quote': c.toggleBlockquote().run(); break;
|
|
case 'link': {
|
|
const url = prompt('URL del link:');
|
|
if (url) c.setLink({ href: url }).run();
|
|
break;
|
|
}
|
|
case 'image': {
|
|
const input = document.createElement('input');
|
|
input.type = 'file'; input.accept = 'image/*';
|
|
input.onchange = async () => {
|
|
const url = input.files?.[0] && await uploadFile(input.files[0]);
|
|
if (url) editor.chain().focus().setImage({ src: url }).run();
|
|
};
|
|
input.click();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById('cover-file')!.addEventListener('change', async (e) => {
|
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
if (!file) return;
|
|
const url = await uploadFile(file);
|
|
if (url) {
|
|
(form.elements.namedItem('cover') as HTMLInputElement).value = url;
|
|
document.getElementById('cover-preview')!.innerHTML = `<img src="${url}" style="max-width:200px">`;
|
|
}
|
|
});
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const id = form.dataset.id;
|
|
const body = {
|
|
title: (form.elements.namedItem('title') as HTMLInputElement).value,
|
|
slug: (form.elements.namedItem('slug') as HTMLInputElement).value,
|
|
category: (form.elements.namedItem('category') as HTMLSelectElement).value,
|
|
excerpt: (form.elements.namedItem('excerpt') as HTMLTextAreaElement).value,
|
|
cover: (form.elements.namedItem('cover') as HTMLInputElement).value || null,
|
|
draft: (form.elements.namedItem('draft') as HTMLInputElement).checked,
|
|
body_html: editor.getHTML(),
|
|
};
|
|
const res = await fetch(id ? `/api/admin/posts/${id}` : '/api/admin/posts', {
|
|
method: id ? 'PUT' : 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const msg = document.getElementById('save-msg')!;
|
|
if (res.ok) {
|
|
msg.textContent = 'Salvato ✓';
|
|
if (!id) location.href = '/admin';
|
|
} else {
|
|
msg.textContent = (await res.json()).error ?? 'Errore di salvataggio';
|
|
}
|
|
});
|
|
</script>
|