summaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-04 16:15:47 +0200
committerivar <i@oiee.no>2026-04-04 16:15:47 +0200
commite952d06567369a0511a711c0e5d3f92dce3fb7eb (patch)
treef5db88c0778f858f4b37f956aea67ae2937b9e56 /internal
parent1fdf7dffa8b3be0eac61796a39a0e4af61713d7a (diff)
downloadnebbet.no-e952d06567369a0511a711c0e5d3f92dce3fb7eb.tar.xz
nebbet.no-e952d06567369a0511a711c0e5d3f92dce3fb7eb.zip
feat: add editable slug field to admin form with JS auto-population
Diffstat (limited to 'internal')
-rw-r--r--internal/admin/templates/form.html81
1 files changed, 56 insertions, 25 deletions
diff --git a/internal/admin/templates/form.html b/internal/admin/templates/form.html
index 1b577c9..61c7cac 100644
--- a/internal/admin/templates/form.html
+++ b/internal/admin/templates/form.html
@@ -1,19 +1,28 @@
{{define "form-content"}}
<h1>{{.Title}}</h1>
- <form method="POST" action="{{.Action}}">
+ <form method="POST" action="{{.Action}}" id="postForm">
<label for="title">Title</label>
<input type="text" id="title" name="title" value="{{.Post.Title}}" required autofocus>
+ <label for="slug">Slug</label>
+ <input type="text" id="slug" name="slug" value="{{.Post.Slug}}" placeholder="auto-generated from title">
+ <p class="hint">URL path for this post (e.g. "my-post" → /my-post). Lowercase letters, numbers, and hyphens only.</p>
+
<label for="date">Date</label>
<input type="date" id="date" name="date" value="{{.Post.Date}}">
<label for="tags">Tags</label>
- <input type="text" id="tags" name="tags" value="{{.Post.Tags}}" placeholder="tag1, tag2, tag3">
+ <input type="text" id="tags" name="tags" value="{{range $i, $t := .Post.Tags}}{{if $i}}, {{end}}{{$t}}{{end}}" placeholder="tag1, tag2, tag3">
<p class="hint">Comma-separated list of tags.</p>
- <label for="content">Content (Markdown)</label>
- <textarea id="content" name="content" style="display: none;">{{.Post.Content}}</textarea>
+ <label for="draft">
+ <input type="checkbox" id="draft" name="draft"{{if .Post.Draft}} checked{{end}}>
+ Draft (not published)
+ </label>
+
+ <label for="blocks">Content</label>
<div id="editor" style="border: 1px solid #ccc; border-radius: 4px; min-height: 340px;"></div>
+ <input type="hidden" id="blocks" name="blocks" value="">
<div class="form-actions">
<button type="submit" class="btn btn-primary">
@@ -24,39 +33,61 @@
</form>
<script type="module">
- import { Crepe } from '@milkdown/crepe';
+ import { EditorJS, Header, Paragraph, List, Code, Quote, ImageTool } from 'editorjs-bundle';
+ import ComponentTool from '/assets/admin/lib/component-tool.js';
+
+ const blocksField = document.getElementById('blocks');
+ const form = document.getElementById('postForm');
+ const titleInput = document.getElementById('title');
+ const slugInput = document.getElementById('slug');
- const contentField = document.getElementById('content');
- const editorContainer = document.getElementById('editor');
- const form = contentField.closest('form');
+ // Auto-populate slug from title for new posts only, until user edits slug manually.
+ {{if .IsNew}}
+ let slugManuallyEdited = slugInput.value !== '';
+ slugInput.addEventListener('input', () => { slugManuallyEdited = true; });
+ titleInput.addEventListener('input', () => {
+ if (slugManuallyEdited) return;
+ slugInput.value = titleInput.value
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+ });
+ {{end}}
+
+ let initialData = [];
+ try {
+ initialData = JSON.parse(blocksField.value || '[]');
+ } catch (e) {
+ console.error('Failed to parse blocks JSON:', e);
+ }
- // Initialize Crepe with the textarea content
- const crepe = new Crepe({
- root: editorContainer,
- defaultValue: contentField.value,
+ const editor = new EditorJS({
+ holder: 'editor',
+ tools: {
+ header: Header,
+ paragraph: Paragraph,
+ list: List,
+ code: Code,
+ quote: Quote,
+ image: ImageTool,
+ component: ComponentTool,
+ },
+ data: { blocks: initialData },
});
- // Sync editor content back to textarea before form submission
+ // Sync editor content back to hidden field before form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
try {
- // Get the markdown content from Crepe
- const markdown = await crepe.getMarkdown();
- contentField.value = markdown;
+ const editorData = await editor.save();
+ blocksField.value = JSON.stringify(editorData);
} catch (err) {
- console.error('Failed to get markdown from editor:', err);
+ console.error('Failed to save editor data:', err);
+ return;
}
- // Submit the form
form.submit();
});
</script>
-
- <noscript>
- <style>
- #editor { display: none; }
- #content { display: block !important; }
- </style>
- </noscript>
{{end}}