Add Markdown editor to admin area

This commit is contained in:
Daniel Nitsikopoulos
2024-09-29 17:09:23 +10:00
parent 37a4508ec4
commit be63fb49c7
12 changed files with 2039 additions and 60 deletions

View File

@@ -0,0 +1,13 @@
module Admin
module Actions
module Posts
class New < Action
include Deps["views.posts.new"]
def handle(req, res)
res.render new
end
end
end
end
end

View File

@@ -0,0 +1,3 @@
.milkdown .ProseMirror {
padding: 0 !important;
}

View File

@@ -3,6 +3,36 @@ import "@main/css/app.css";
import "@app/builds/tailwind.css";
import "../css/app.css";
import { Crepe } from "@milkdown/crepe";
import { listener, listenerCtx } from "@milkdown/kit/plugin/listener";
import "@milkdown/crepe/theme/common/style.css";
// We have some themes for you to choose
import "@milkdown/crepe/theme/frame.css";
async function uploadImage(file: File) {
const formData = new FormData();
formData.append("file", file); // Append the file to the FormData object
try {
const response = await fetch("/micropub/media", {
method: "POST",
body: formData,
});
if (response.ok) {
const jsonResponse = await response.json();
return jsonResponse["url"];
} else {
alert("File upload failed.");
}
} catch (error) {
console.error("Error:", error);
alert("An error occurred during the upload.");
return null;
}
}
(function () {
document.addEventListener("alpine:init", () => {
Alpine.magic("clipboard", () => {
@@ -32,5 +62,31 @@ import "../css/app.css";
},
};
});
let editor = document.getElementById("editor");
const crepe = new Crepe({
root: editor,
defaultValue: editor.dataset.postText,
featureConfigs: {
[Crepe.Feature.ImageBlock]: {
onUpload: async (file: File) => {
return uploadImage(file);
},
},
},
});
crepe.editor.config((ctx) => {
const bodyText = document.getElementById("body");
bodyText.hidden = true;
ctx.get(listenerCtx).markdownUpdated((ctx, markdown, prevMarkdown) => {
bodyText.innerHTML = markdown;
});
});
crepe.editor.use(listener);
crepe.create();
});
})();

View File

@@ -46,6 +46,8 @@ module Admin
post "/bookmarks/:id/mark_unread", to: Auth.call(action: "bookmarks.mark_unread")
get "/posts", to: Auth.call(action: "posts.index")
get "/posts/new", to: Auth.call(action: "posts.new")
post "/posts/create", to: Auth.call(action: "posts.create")
delete "/posts/:id", to: Auth.call(action: "posts.delete")
post "/posts/:id/archive", to: Auth.call(action: "posts.archive")
post "/posts/:id/publish", to: Auth.call(action: "posts.publish")

View File

@@ -5,6 +5,8 @@ div class="max-w-prose mx-auto" x-data="{ activeTab: 0 }"
div class="flex"
a href="#" class="text-gray-200 cursor-pointer p-2 border-2 mr-2 rounded border-blue-400" :class="{ 'bg-blue-400 text-blue-900': activeTab === 0 }" @click="activeTab = 0" class="tab-control" Published
a href="#" class="text-gray-200 cursor-pointer p-2 border-2 rounded border-blue-400" :class="{ 'bg-blue-400 text-blue-900': activeTab === 1 }" @click="activeTab = 1" class="tab-control" Un published
div class="flex-1"
a href="/admin/posts/new" class="text-gray-200 cursor-pointer p-2 border-2 rounded border-blue-400 bg-blue-400 text-blue-900" New Post
table class="prose dark:prose-invert table-auto prose-a:text-blue-600 prose-a:no-underline"
thead
th Details
@@ -54,6 +56,3 @@ div class="max-w-prose mx-auto" x-data="{ activeTab: 0 }"
button hx-post="/admin/posts/#{post.id}/publish" publish
div class="max-w-screen-md mx-auto border-t border-solid border-gray-200 dark:border-gray-600"

View File

@@ -0,0 +1,26 @@
article x-data="{postTitle: 'Post title', postSlug: 'post-title', slugify(event) {
var str = event.target.value.replace(/^\s+|\s+$/g, '');
str = str.toLowerCase();
str = str.replace(/[^a-z0-9 -]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
this.postSlug = str;
}}" class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-img:rounded"
form hx-post="/micropub"
fieldset class="mb-4 flex"
label for="name" class="mr-2"
input type="text" name="name" id="name" class="text-3xl w-full" x-on:change.debounce="slugify($event)" x-model="postTitle"
fieldset class="mb-4 flex"
label for="slug" class="mr-2" Slug:
input type="text" name="slug" id="slug" class="w-full px-1 border rounded" x-model="postSlug"
div id="editor" data-post-text=""
textarea id="body" name="content" class="text-gray-800 w-full border-blue-200 border-2 rounded p-2 mb-4" x-data="{ resize: () => { $el.style.height = '5px'; $el.style.height = $el.scrollHeight + 'px' } }" x-init="resize()" @input="resize()"
// fieldset class="mb-4 flex"
// label for="commentable" class="mr-2" Commentable?
// input class="mt-2" type="checkbox" value="true" id="commentable" name="commentable" switch="switch" checked=true
fieldset class="mb-4 flex"
label for="tags" class="mr-2" Tags:
input type="text" name="category" id="tags" class="w-full px-1 border rounded" value=""
button class="rounded bg-blue-100 hover:bg-blue-200 text-blue-600 px-2 hover:cursor-pointer" type="submit"
= "Create"

View File

@@ -30,8 +30,12 @@ article x-data="$textHighlighter, {isOpen: false, anchorX: 0, anchorY: 0, text:
h1= post.name || "💬"
form action="/admin/post/#{post.id}/update" method="POST"
- if post.post_type != "bookmark"
textarea name="body" class="text-gray-800 w-full border-blue-200 border-2 rounded p-2 mb-4" x-data="{ resize: () => { $el.style.height = '5px'; $el.style.height = $el.scrollHeight + 'px' } }" x-init="resize()" @input="resize()"
== markdown_body
fieldset class="mb-4 flex"
label for="slug" class="mr-2" Slug:
input type="text" name="slug" id="slug" class="w-full px-1 border rounded" value="#{post.slug}"
div id="editor" data-post-text="#{markdown_body}"
textarea id="body" name="body" class="text-gray-800 w-full border-blue-200 border-2 rounded p-2 mb-4" x-data="{ resize: () => { $el.style.height = '5px'; $el.style.height = $el.scrollHeight + 'px' } }" x-init="resize()" @input="resize()"
// == markdown_body
- if post.post_type == "bookmark"
div x-ref="bookmarkText" @mouseup.capture="highlightText()"
== post.cached_content

View File

@@ -0,0 +1,21 @@
module Admin
module Views
module Posts
class New < Admin::View
include Deps["repos.post_repo"]
expose :published_posts do |posts|
posts[0]
end
expose :unpublished_posts do |posts|
posts[1]
end
expose :posts do
post_repo.list.partition { |p| p.published_at }
end
end
end
end
end