From ca8b4ae5afc38f37ea39edd38b7679c0c9ed399e Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Sun, 14 Apr 2024 18:52:16 -0500 Subject: [PATCH] Post Creation and Manipulation Uploading images now easier. Just drag and drop onto the text area. Signed-off-by: Armored Dragon --- backend/core/core.js | 78 +++--------- backend/core/external_api.js | 1 - backend/core/internal_api.js | 7 +- backend/page_scripts.js | 19 +-- frontend/public/js/newBlog.js | 40 +++--- frontend/views/themes/default/css/generic.css | 9 ++ .../views/themes/default/css/generic.scss | 8 ++ frontend/views/themes/default/css/newPost.css | 99 +++++++++++++++ .../views/themes/default/css/newPost.scss | 119 ++++++++++++++++++ frontend/views/themes/default/css/post.css | 7 +- frontend/views/themes/default/css/post.scss | 10 +- .../themes/default/ejs/partials/post.ejs | 5 +- frontend/views/themes/default/ejs/post.ejs | 12 ++ frontend/views/themes/default/ejs/postNew.ejs | 79 ++++++++++++ .../default/img/textarea/sidebyside.svg | 38 ++++++ .../themes/default/img/textarea/video.svg | 38 ++++++ frontend/views/themes/default/js/newPost.js | 111 ++++++++++++++++ frontend/views/themes/default/js/post.js | 6 + yab.js | 15 +-- 19 files changed, 590 insertions(+), 111 deletions(-) create mode 100644 frontend/views/themes/default/css/newPost.css create mode 100644 frontend/views/themes/default/css/newPost.scss create mode 100644 frontend/views/themes/default/ejs/postNew.ejs create mode 100644 frontend/views/themes/default/img/textarea/sidebyside.svg create mode 100644 frontend/views/themes/default/img/textarea/video.svg create mode 100644 frontend/views/themes/default/js/newPost.js create mode 100644 frontend/views/themes/default/js/post.js diff --git a/backend/core/core.js b/backend/core/core.js index 501d116..f4a84a0 100644 --- a/backend/core/core.js +++ b/backend/core/core.js @@ -4,6 +4,7 @@ const sharp = require("sharp"); const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsCommand, DeleteObjectsCommand } = require("@aws-sdk/client-s3"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); let s3; +const crypto = require("crypto"); const md = require("markdown-it")() .use(require("markdown-it-underline")) .use(require("markdown-it-footnote")) @@ -195,27 +196,6 @@ async function postBlog(blog_post, owner_id) { // Save to database const database_blog = await prisma.blogPost.create({ data: { ...blog_post_formatted, owner: { connect: { id: owner_id } } } }); - // Init image vars - let uploaded_images = []; - let uploaded_thumbnail = "DEFAULT"; - - // For Each image, upload to S3 - if (blog_post.images) { - for (let i = 0; blog_post.images.length > i; i++) { - const image = blog_post.images[i]; - const image_data = Buffer.from(image.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(database_blog.id, "blog", false, image_data, image.id); - if (name) uploaded_images.push(name); - } - } - - // Upload thumbnail to S3 - if (blog_post.thumbnail) { - const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(database_blog.id, "blog", true, image_data, blog_post.thumbnail.id); - if (name) uploaded_thumbnail = name; - } - // Update the blog post to include references to our images await prisma.blogPost.update({ where: { id: database_blog.id }, data: { images: uploaded_images, thumbnail: uploaded_thumbnail } }); return { success: true, blog_id: database_blog.id }; @@ -256,7 +236,6 @@ async function updateBlog(blog_post, requester_id) { const [hour, minute] = blog_post.time.split(":"); publish_date = new Date(year, month - 1, day, hour, minute); } - let blog_post_formatted = { title: blog_post.title, description: blog_post.description, @@ -264,37 +243,11 @@ async function updateBlog(blog_post, requester_id) { visibility: blog_post.unlisted ? "UNLISTED" : "PUBLISHED", publish_date: publish_date || blog_post.publish_date, tags: blog_post.tags, + images: [...post.data.raw_images, ...blog_post.images], }; await prisma.blogPost.update({ where: { id: post.data.id }, data: blog_post_formatted }); - let uploaded_images = []; - let uploaded_thumbnail = "DEFAULT"; - - // For Each image, upload to S3 - if (blog_post.images) { - for (let i = 0; blog_post.images.length > i; i++) { - const image = blog_post.images[i]; - const image_data = Buffer.from(image.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(post.id, "blog", false, image_data, image.id); - if (name) uploaded_images.push(name); - } - } - - let data_to_update = { - images: [...post.data.raw_images, ...uploaded_images], - }; - - if (blog_post.thumbnail) { - const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(post.data.id, "blog", true, image_data, blog_post.thumbnail.id); - if (name) uploaded_thumbnail = name; - - data_to_update.thumbnail = uploaded_thumbnail; - } - - await prisma.blogPost.update({ where: { id: post.data.id }, data: data_to_update }); - return { success: true }; } async function deleteImage(image, requester_id) { @@ -323,19 +276,18 @@ async function deleteImage(image, requester_id) { return { success: true }; } -async function _uploadImage(parent_id, parent_type, is_thumbnail, buffer, name) { +async function postImage(post_id, buffer) { if (!use_s3_storage) return null; let size = { width: 1920, height: 1080 }; - if (is_thumbnail) size = { width: 300, height: 300 }; + const image_name = crypto.randomUUID(); - const compressed_image = await sharp(buffer, { animated: true }) + const compressed_image = await sharp(Buffer.from(buffer.split(",")[1], "base64"), { animated: true }) .resize({ ...size, withoutEnlargement: true, fit: "inside" }) .webp({ quality: 90, animated: true }) .toBuffer(); - const params = { Bucket: process.env.S3_BUCKET_NAME, - Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${name}.webp`, + Key: `${process.env.ENVIRONMENT}/posts/${post_id}/${image_name}.webp`, Body: compressed_image, ContentType: "image/webp", }; @@ -343,7 +295,7 @@ async function _uploadImage(parent_id, parent_type, is_thumbnail, buffer, name) const command = new PutObjectCommand(params); await s3.send(command); - return name; + return image_name; } async function _getImage(parent_id, parent_type, name) { if (!use_s3_storage) return null; @@ -383,7 +335,7 @@ async function _deleteS3Directory(id, type) { // If there are more objects to delete (truncated result), recursively call the function again // if (listed_objects.IsTruncated) await emptyS3Directory(bucket, dir); } -async function _renderPost(blog_post, raw, { post_type = "blog" } = {}) { +async function _renderPost(blog_post, raw) { if (raw) { // Had to do this, only God knows why. blog_post.raw_images = []; @@ -392,17 +344,13 @@ async function _renderPost(blog_post, raw, { post_type = "blog" } = {}) { blog_post.raw_thumbnail = blog_post.thumbnail; blog_post.raw_content = blog_post.content; } - if (blog_post.images) { // Get the image urls for the post for (i = 0; blog_post.images.length > i; i++) { - blog_post.images[i] = await _getImage(blog_post.id, post_type, blog_post.images[i]); + blog_post.images[i] = await _getImage(blog_post.id, "posts", blog_post.images[i]); } } - // get thumbnail URL - blog_post.thumbnail = await _getImage(blog_post.id, post_type, blog_post.thumbnail); - if (blog_post.content) { // Render the markdown contents of the post blog_post.content = md.render(blog_post.content); @@ -487,6 +435,12 @@ async function _getSettings() { return (settings[key] = value); }); } +// Create a new empty "post". +// Used so uploaded images know where to go +async function newPost(owner_id) { + const post = await prisma.blogPost.create({ data: { owner: { connect: { id: owner_id } } } }); + return post.id; +} async function getSetting(key, { parse = true }) { if (!settings[key]) return null; @@ -513,4 +467,4 @@ async function postSetting(key, value) { } } -module.exports = { settings, registerUser, getUser, getAuthorPage, postBlog, updateBlog, getBlog, deleteBlog, deleteImage, postSetting, getSetting }; +module.exports = { settings, newPost, registerUser, getUser, getAuthorPage, postBlog, updateBlog, getBlog, deleteBlog, postImage, deleteImage, postSetting, getSetting }; diff --git a/backend/core/external_api.js b/backend/core/external_api.js index 457c1ee..83f61c2 100644 --- a/backend/core/external_api.js +++ b/backend/core/external_api.js @@ -1,7 +1,6 @@ const feed_lib = require("feed").Feed; const core = require("./core"); -// TODO: Expose ATOM Feed items function getBaseFeed() { return new feed_lib({ title: core.settings.WEBSITE_NAME, diff --git a/backend/core/internal_api.js b/backend/core/internal_api.js index cf1f052..597991e 100644 --- a/backend/core/internal_api.js +++ b/backend/core/internal_api.js @@ -43,6 +43,11 @@ async function postSetting(request, response) { response.json(await core.postSetting(request.body.setting_name, request.body.value)); } +async function postImage(request, response) { + // TODO: Permissions for uploading images + // TODO: Verification for image uploading + return response.json(await core.postImage(request.body.post_id, request.body.buffer)); +} async function deleteImage(req, res) { // TODO: Permissions for deleting image return res.json(await core.deleteImage(req.body, req.session.user.id)); @@ -79,4 +84,4 @@ async function patchBlog(req, res) { return res.json(await core.updateBlog({ ...valid.data, id: req.body.id }, req.session.user.id)); } -module.exports = { postRegister, postLogin, postSetting, deleteImage, postBlog, deleteBlog, patchBlog }; +module.exports = { postRegister, postLogin, postSetting, postImage, deleteImage, postBlog, deleteBlog, patchBlog }; diff --git a/backend/page_scripts.js b/backend/page_scripts.js index b453a08..43068af 100644 --- a/backend/page_scripts.js +++ b/backend/page_scripts.js @@ -4,12 +4,10 @@ const core = require("./core/core"); function getThemePage(page_name) { return `themes/${core.settings.theme}/ejs/${page_name}.ejs`; } - function getDefaults(req) { // TODO: Fix reference to website_name return { logged_in_user: req.session.user, website_name: core.settings.WEBSITE_NAME || "Yet-Another-Blog", settings: core.settings }; } - async function index(request, response) { // Check if the master admin has been created // const is_setup_complete = core.settings["SETUP_COMPLETE"]; @@ -53,18 +51,9 @@ async function blogSingle(req, res) { if (blog.success === false) return res.redirect("/"); res.render(getThemePage("post"), { ...getDefaults(req), blog_post: blog.data }); } -function blogNew(request, response) { - // TODO: Turn date formatting into function - let existing_blog = {}; - let published_date_parts = new Date().toLocaleDateString().split("/"); - const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`; - existing_blog.publish_date = formatted_date; - - let published_time_parts = new Date().toLocaleTimeString([], { timeStyle: "short" }).slice(0, 4).split(":"); - const formatted_time = `${published_time_parts[0].padStart(2, "0")}:${published_time_parts[1].padStart(2, "0")}`; - existing_blog.publish_time = formatted_time; - - response.render("blogNew.ejs", { ...getDefaults(request), existing_blog: existing_blog }); +async function blogNew(request, response) { + const new_post = await core.newPost(request.session.user.id); + return response.redirect(`/post/${new_post}/edit`); } async function blogEdit(req, res) { let existing_blog = await core.getBlog({ id: req.params.blog_id, raw: true }); @@ -78,7 +67,7 @@ async function blogEdit(req, res) { const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`; existing_blog.publish_date = formatted_date; - res.render("blogNew.ejs", { ...getDefaults(req), existing_blog: existing_blog }); + res.render(getThemePage("postNew"), { ...getDefaults(req), existing_blog: existing_blog }); } async function admin(request, response) { response.render(getThemePage("admin-settings"), { ...getDefaults(request) }); diff --git a/frontend/public/js/newBlog.js b/frontend/public/js/newBlog.js index 0a1245a..d7b1f72 100644 --- a/frontend/public/js/newBlog.js +++ b/frontend/public/js/newBlog.js @@ -6,7 +6,7 @@ let pending_thumbnail = {}; const thumbnail_area = qs(".e-thumbnail"); const image_area = qs(".e-image-area"); -const text_area = qs(".e-content textarea"); +const post_content_area = qs(".e-content textarea"); // Style function stylizeDropArea(element) { @@ -27,13 +27,13 @@ function stylizeDropArea(element) { } // Auto resize on page load -text_area.style.height = text_area.scrollHeight + "px"; -text_area.style.minHeight = text_area.scrollHeight + "px"; +post_content_area.style.height = post_content_area.scrollHeight + "px"; +post_content_area.style.minHeight = post_content_area.scrollHeight + "px"; // Auto expand blog area -text_area.addEventListener("input", (e) => { - text_area.style.height = text_area.scrollHeight + "px"; - text_area.style.minHeight = e.target.scrollHeight + "px"; +post_content_area.addEventListener("input", (e) => { + post_content_area.style.height = post_content_area.scrollHeight + "px"; + post_content_area.style.minHeight = e.target.scrollHeight + "px"; }); stylizeDropArea(thumbnail_area); @@ -204,12 +204,12 @@ qs("#insert-sup").addEventListener("click", () => textareaAction("^", undefined, function textareaAction(insert, cursor_position, dual_side) { // Insert the custom string at the cursor position - const selectionStart = text_area.selectionStart; - const selectionEnd = text_area.selectionEnd; + const selectionStart = post_content_area.selectionStart; + const selectionEnd = post_content_area.selectionEnd; - const textBefore = text_area.value.substring(0, selectionStart); - const textAfter = text_area.value.substring(selectionEnd); - const selectedText = text_area.value.substring(selectionStart, selectionEnd); + const textBefore = post_content_area.value.substring(0, selectionStart); + const textAfter = post_content_area.value.substring(selectionEnd); + const selectedText = post_content_area.value.substring(selectionStart, selectionEnd); let updatedText; @@ -219,34 +219,34 @@ function textareaAction(insert, cursor_position, dual_side) { updatedText = `${textBefore}${insert}${selectedText}${textAfter}`; } - text_area.value = updatedText; + post_content_area.value = updatedText; // Set the cursor position after the custom string qs(".e-content textarea").focus(); const newPosition = selectionStart + (cursor_position || insert.length); - text_area.setSelectionRange(newPosition, newPosition); + post_content_area.setSelectionRange(newPosition, newPosition); } -text_area.addEventListener("drop", (event) => { +post_content_area.addEventListener("drop", (event) => { event.preventDefault(); // Get the custom string from the drag data const customString = `\{image:${event.dataTransfer.getData("text/plain")}\}\n`; // Insert the custom string at the cursor position - const selectionStart = text_area.selectionStart; - const selectionEnd = text_area.selectionEnd; + const selectionStart = post_content_area.selectionStart; + const selectionEnd = post_content_area.selectionEnd; - const textBefore = text_area.value.substring(0, selectionStart); - const textAfter = text_area.value.substring(selectionEnd); + const textBefore = post_content_area.value.substring(0, selectionStart); + const textAfter = post_content_area.value.substring(selectionEnd); const updatedText = textBefore + customString + textAfter; - text_area.value = updatedText; + post_content_area.value = updatedText; // Set the cursor position after the custom string const newPosition = selectionStart + customString.length; - text_area.setSelectionRange(newPosition, newPosition); + post_content_area.setSelectionRange(newPosition, newPosition); }); // Load the existing images into our existing_images variable diff --git a/frontend/views/themes/default/css/generic.css b/frontend/views/themes/default/css/generic.css index 74d5564..c478a5a 100644 --- a/frontend/views/themes/default/css/generic.css +++ b/frontend/views/themes/default/css/generic.css @@ -85,6 +85,10 @@ body { background-color: #e70404; } +.button.caution { + background-color: #6d6d6c; +} + .button.disabled { filter: contrast(50%); filter: brightness(50%); @@ -139,6 +143,11 @@ body { margin: auto 5px auto auto; } +.separator { + border-bottom: 2px solid #b3b3b3; + margin-bottom: 1rem; +} + @media screen and (max-width: 1280px) { .page-center { width: 95%; diff --git a/frontend/views/themes/default/css/generic.scss b/frontend/views/themes/default/css/generic.scss index b0cafab..2430c7a 100644 --- a/frontend/views/themes/default/css/generic.scss +++ b/frontend/views/themes/default/css/generic.scss @@ -89,6 +89,9 @@ body { .button.bad { background-color: #e70404; } +.button.caution { + background-color: #6d6d6c; +} .button.disabled { filter: contrast(50%); filter: brightness(50%); @@ -145,6 +148,11 @@ body { margin: auto 5px auto auto; } +.separator { + border-bottom: 2px solid #b3b3b3; + margin-bottom: 1rem; +} + @media screen and (max-width: 1280px) { .page-center { width: 95%; diff --git a/frontend/views/themes/default/css/newPost.css b/frontend/views/themes/default/css/newPost.css new file mode 100644 index 0000000..23c673f --- /dev/null +++ b/frontend/views/themes/default/css/newPost.css @@ -0,0 +1,99 @@ +.page .page-center { + background-color: white; + min-height: 100px; + box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px; + margin-top: 2rem; + padding: 1rem; + box-sizing: border-box; + width: 1080px; + max-width: 1080px; + flex-direction: column; + border-radius: 5px; +} +.page .page-center .page-title { + text-align: center; + font-size: 1.5rem; +} +.page .page-center .info-container { + display: flex; + flex-direction: column; + margin-bottom: 1rem; +} +.page .page-center .info-container input { + width: 100%; + padding: 0.5rem; + box-sizing: border-box; + background-color: rgb(245, 245, 245); + border: 1px solid gray; + border-radius: 5px; +} +.page .page-center .info-container textarea { + padding: 0.5rem; + box-sizing: border-box; + max-width: 100%; + min-width: 100%; + min-height: 5rem; + background-color: rgb(245, 245, 245); + border: 1px solid gray; +} +.page .page-center .side-by-side-info { + display: flex; + flex-direction: row; +} +.page .page-center .side-by-side-info input { + margin: 0 0.25rem; +} +.page .page-center .side-by-side-info .button { + padding: 0.5rem; + box-sizing: border-box; + margin: auto; +} + +.rich-text-editor .controls { + width: 100%; + min-height: 10px; + background-color: #dbd8d8; + display: flex; + flex-direction: row; +} +.rich-text-editor .controls a { + box-sizing: border-box; + margin: 0.1rem; + cursor: pointer; + background-color: #dbd8d8; + display: flex; + flex-direction: row; + width: 2rem; + height: 2rem; +} +.rich-text-editor .controls a span { + margin: auto; +} +.rich-text-editor .controls a:hover, +.rich-text-editor .controls a:focus { + filter: brightness(80%); +} +.rich-text-editor .controls .left { + margin: 0 auto 0 0; + height: 100%; + display: flex; + flex-direction: row; +} +.rich-text-editor .controls .right { + margin: 0 0 0 auto; + display: flex; + flex-direction: row; +} +.rich-text-editor .controls .right a { + padding: 2px; + box-sizing: border-box; + display: flex; + flex-direction: row; +} +.rich-text-editor .controls .right a img { + height: 20px; + margin: auto; +} +.rich-text-editor textarea { + border-radius: 0 0 5px 5px; +} \ No newline at end of file diff --git a/frontend/views/themes/default/css/newPost.scss b/frontend/views/themes/default/css/newPost.scss new file mode 100644 index 0000000..683a54c --- /dev/null +++ b/frontend/views/themes/default/css/newPost.scss @@ -0,0 +1,119 @@ +.page .page-center { + background-color: white; + min-height: 100px; + box-shadow: #0000001c 0 0px 5px; + margin-top: 2rem; + padding: 1rem; + box-sizing: border-box; + width: 1080px; + max-width: 1080px; + flex-direction: column; + border-radius: 5px; + + .page-title { + text-align: center; + font-size: 1.5rem; + } + + .info-container { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + + .title { + } + + input { + width: 100%; + padding: 0.5rem; + box-sizing: border-box; + background-color: rgb(245, 245, 245); + border: 1px solid gray; + border-radius: 5px; + } + textarea { + padding: 0.5rem; + box-sizing: border-box; + max-width: 100%; + min-width: 100%; + min-height: 5rem; + background-color: rgb(245, 245, 245); + border: 1px solid gray; + // border-radius: 5px; + } + } + + .side-by-side-info { + display: flex; + flex-direction: row; + + input { + margin: 0 0.25rem; + } + + .button { + padding: 0.5rem; + box-sizing: border-box; + margin: auto; + } + } +} + +.rich-text-editor { + .controls { + width: 100%; + min-height: 10px; + background-color: #dbd8d8; + display: flex; + flex-direction: row; + + a { + box-sizing: border-box; + margin: 0.1rem; + cursor: pointer; + background-color: #dbd8d8; + display: flex; + flex-direction: row; + width: 2rem; + height: 2rem; + + span { + margin: auto; + } + } + + a:hover, + a:focus { + filter: brightness(80%); + } + + .left { + margin: 0 auto 0 0; + height: 100%; + display: flex; + flex-direction: row; + } + + .right { + margin: 0 0 0 auto; + display: flex; + flex-direction: row; + + a { + padding: 2px; + box-sizing: border-box; + display: flex; + flex-direction: row; + + img { + height: 20px; + margin: auto; + } + } + } + } + + textarea { + border-radius: 0 0 5px 5px; + } +} diff --git a/frontend/views/themes/default/css/post.css b/frontend/views/themes/default/css/post.css index 9399d19..8a63dd4 100644 --- a/frontend/views/themes/default/css/post.css +++ b/frontend/views/themes/default/css/post.css @@ -1,6 +1,5 @@ .page .page-center { background-color: white; - min-height: 100px; box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px; margin-top: 2rem; padding: 1rem; @@ -19,4 +18,10 @@ } .page .page-center .image-container img { max-width: 100%; +} +.page .page-center .horizontal-button-container { + width: 100%; +} +.page .page-center .horizontal-button-container button { + height: 2rem; } \ No newline at end of file diff --git a/frontend/views/themes/default/css/post.scss b/frontend/views/themes/default/css/post.scss index a7f1557..49e54f2 100644 --- a/frontend/views/themes/default/css/post.scss +++ b/frontend/views/themes/default/css/post.scss @@ -1,6 +1,6 @@ .page .page-center { background-color: white; - min-height: 100px; + // min-height: 100px; box-shadow: #0000001c 0 0px 5px; margin-top: 2rem; padding: 1rem; @@ -22,4 +22,12 @@ max-width: 100%; } } + + .horizontal-button-container { + width: 100%; + + button { + height: 2rem; + } + } } diff --git a/frontend/views/themes/default/ejs/partials/post.ejs b/frontend/views/themes/default/ejs/partials/post.ejs index f114bdd..10f4695 100644 --- a/frontend/views/themes/default/ejs/partials/post.ejs +++ b/frontend/views/themes/default/ejs/partials/post.ejs @@ -1,12 +1,11 @@
- <%= post.title %> + <%= post.title ? post.title : "Untitled Post" %>
<%= post.description %>
-
<%= post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) %>
+
<%= post.publish_date ? post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) : "Null" %>
Null minute read
-
Null words
diff --git a/frontend/views/themes/default/ejs/post.ejs b/frontend/views/themes/default/ejs/post.ejs index dbda94b..828898d 100644 --- a/frontend/views/themes/default/ejs/post.ejs +++ b/frontend/views/themes/default/ejs/post.ejs @@ -4,11 +4,21 @@ + Yet-Another-Blog <%- include("partials/header.ejs") %>
+ <%if(logged_in_user) {%> +
+
+ + +
+
+ <%}%> +
<%= blog_post.title%>
<%- blog_post.content %> @@ -17,3 +27,5 @@ <%- include("partials/footer.ejs") %> + + diff --git a/frontend/views/themes/default/ejs/postNew.ejs b/frontend/views/themes/default/ejs/postNew.ejs new file mode 100644 index 0000000..491d276 --- /dev/null +++ b/frontend/views/themes/default/ejs/postNew.ejs @@ -0,0 +1,79 @@ + + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> +
+
+
New Post
+
+
Title
+ +
+
+
Description
+ +
+
+
Tags
+ +
+
+
+
Content
+
+
+ +
+ + +
+
+ +
+
+
+
Post Date
+
+ + +
+
+
+
+ + +
+
+
+
+ <%- include("partials/footer.ejs") %> + + + diff --git a/frontend/views/themes/default/img/textarea/sidebyside.svg b/frontend/views/themes/default/img/textarea/sidebyside.svg new file mode 100644 index 0000000..c473cb1 --- /dev/null +++ b/frontend/views/themes/default/img/textarea/sidebyside.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/views/themes/default/img/textarea/video.svg b/frontend/views/themes/default/img/textarea/video.svg new file mode 100644 index 0000000..b6d234e --- /dev/null +++ b/frontend/views/themes/default/img/textarea/video.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/views/themes/default/js/newPost.js b/frontend/views/themes/default/js/newPost.js new file mode 100644 index 0000000..743b556 --- /dev/null +++ b/frontend/views/themes/default/js/newPost.js @@ -0,0 +1,111 @@ +let blog_id = window.location.href.split("/")[4]; +const post_content_area = qs("#post-content"); +let images = []; + +// TODO: Support videos + +// Text area custom text editor +qs("#insert-sidebyside").addEventListener("click", () => textareaAction("{sidebyside}{/sidebyside}", 12)); +qs("#insert-video").addEventListener("click", () => textareaAction("{video:}", 7)); +qs("#insert-h1").addEventListener("click", () => textareaAction("# ")); +qs("#insert-h2").addEventListener("click", () => textareaAction("## ")); +qs("#insert-h3").addEventListener("click", () => textareaAction("### ")); +qs("#insert-h4").addEventListener("click", () => textareaAction("#### ")); +qs("#insert-underline").addEventListener("click", () => textareaAction("_", undefined, true)); +qs("#insert-italics").addEventListener("click", () => textareaAction("*", undefined, true)); +qs("#insert-bold").addEventListener("click", () => textareaAction("__", undefined, true)); +qs("#insert-strike").addEventListener("click", () => textareaAction("~~", undefined, true)); +qs("#insert-sup").addEventListener("click", () => textareaAction("^", undefined, true)); + +function textareaAction(insert, cursor_position, dual_side) { + // Insert the custom string at the cursor position + const selectionStart = post_content_area.selectionStart; + const selectionEnd = post_content_area.selectionEnd; + + const textBefore = post_content_area.value.substring(0, selectionStart); + const textAfter = post_content_area.value.substring(selectionEnd); + const selectedText = post_content_area.value.substring(selectionStart, selectionEnd); + + let updatedText; + + if (dual_side) updatedText = `${textBefore}${insert}${selectedText}${insert}${textAfter}`; + else updatedText = `${textBefore}${insert}${selectedText}${textAfter}`; + + post_content_area.value = updatedText; + + // Set the cursor position after the custom string + post_content_area.focus(); + const newPosition = selectionStart + (cursor_position || insert.length); + post_content_area.setSelectionRange(newPosition, newPosition); +} + +// Upload an image to the blog post +post_content_area.addEventListener("drop", async (event) => { + event.preventDefault(); + const files = event.dataTransfer.files; + // let image_queue = []; + + for (let i = 0; i < files.length; i++) { + // Each dropped image will be stored in this formatted object + const image_object = { + data_blob: new Blob([await files[i].arrayBuffer()]), + content_type: files[i].type, + }; + let form_data = { + buffer: await _readFile(image_object.data_blob), + post_id: blog_id, + }; + + const image_uploading_request = await request("/api/web/image", "POST", form_data); + + if (image_uploading_request.status == 200) { + textareaAction(`{image:${image_uploading_request.body}}`); + images.push(image_uploading_request.body); + } + } +}); + +// We need to read the file contents in order to convert it to base64 to send to the server +function _readFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(file); + }); +} + +async function publish() { + let form_data = { + title: qs("#post-title").value, + description: qs("#post-description").value, + tags: [], + images: images, + content: qs("#post-content").value, + date: qs("#date").value, + time: qs("#time").value, + id: blog_id, + }; + + // Get our tags, trim them, then shove them into an array + const tags_value = qs("#post-tags").value || ""; + if (tags_value.length) { + let tags_array = qs("#post-tags").value.split(","); + tags_array.forEach((tag) => form_data.tags.push(tag.trim())); + } + + const post_response = await request("/api/web/post", "PATCH", form_data); + + if (post_response.body.success) { + window.location.href = `/post/${post_response.body.post_id}`; + } +} + +// Auto resize on page load +post_content_area.style.height = post_content_area.scrollHeight + "px"; +post_content_area.style.minHeight = post_content_area.scrollHeight + "px"; +// Auto expand blog area +post_content_area.addEventListener("input", (e) => { + post_content_area.style.height = post_content_area.scrollHeight + "px"; + post_content_area.style.minHeight = e.target.scrollHeight + "px"; +}); diff --git a/frontend/views/themes/default/js/post.js b/frontend/views/themes/default/js/post.js new file mode 100644 index 0000000..0250e23 --- /dev/null +++ b/frontend/views/themes/default/js/post.js @@ -0,0 +1,6 @@ +let post_id = window.location.href.split("/")[4]; + +async function deletePost() { + const post_response = await request("/api/web/post", "DELETE", { id: post_id }); +} +function editPost() {} diff --git a/yab.js b/yab.js index 4faa196..f75ae9f 100644 --- a/yab.js +++ b/yab.js @@ -31,10 +31,11 @@ app.use( app.post("/login", checkNotAuthenticated, internal.postLogin); app.post("/register", checkNotAuthenticated, internal.postRegister); app.post("/setting", checkAuthenticated, internal.postSetting); -app.post("/api/web/blog", checkAuthenticated, internal.postBlog); -app.delete("/api/web/blog/image", checkAuthenticated, internal.deleteImage); -app.delete("/api/web/blog", checkAuthenticated, internal.deleteBlog); -app.patch("/api/web/blog", checkAuthenticated, internal.patchBlog); +app.post("/api/web/post", checkAuthenticated, internal.postBlog); +app.post("/api/web/image", checkAuthenticated, internal.postImage); +app.delete("/api/web/post/image", checkAuthenticated, internal.deleteImage); +app.delete("/api/web/post", checkAuthenticated, internal.deleteBlog); +app.patch("/api/web/post", checkAuthenticated, internal.patchBlog); // app.delete("/logout", page_scripts.logout); @@ -45,9 +46,9 @@ app.get("/register", checkNotAuthenticated, page_scripts.register); app.get("/author/:author_id", page_scripts.author); app.get("/admin", checkAuthenticated, page_scripts.admin); app.get("/posts", page_scripts.blogList); -app.get("/blog/new", checkAuthenticated, page_scripts.blogNew); -app.get("/blog/:blog_id", page_scripts.blogSingle); -app.get("/blog/:blog_id/edit", checkAuthenticated, page_scripts.blogEdit); +app.get("/post/new", checkAuthenticated, page_scripts.blogNew); +app.get("/post/:blog_id", page_scripts.blogSingle); +app.get("/post/:blog_id/edit", checkAuthenticated, page_scripts.blogEdit); app.get("/atom", page_scripts.atom); app.get("/json", page_scripts.jsonFeed);