Post Creation and Manipulation
Uploading images now easier. Just drag and drop onto the text area. Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>pull/1/head
parent
5ab8a79e78
commit
ca8b4ae5af
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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) });
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -20,3 +19,9 @@
|
|||
.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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<div class="post">
|
||||
<a href="/blog/<%= post.id %>" class="title"><%= post.title %></a>
|
||||
<a href="/post/<%= post.id %>" class="title"><%= post.title ? post.title : "Untitled Post" %></a>
|
||||
<div class="authors">By <a href="/author/<%= post.owner.id %>"><%= post.owner.username %></a></div>
|
||||
<div class="description"><%= post.description %></div>
|
||||
<div class="badges">
|
||||
<div class="info">
|
||||
<div class="info-blip icon publish-date"><%= post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) %></div>
|
||||
<div class="info-blip icon publish-date"><%= post.publish_date ? post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) : "Null" %></div>
|
||||
<div class="info-blip icon reading-time">Null minute read</div>
|
||||
<div class="info-blip icon word-count">Null words</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,11 +4,21 @@
|
|||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="../css/post.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../css/generic.css" />
|
||||
<script src="/js/generic.js"></script>
|
||||
<title>Yet-Another-Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<%- include("partials/header.ejs") %>
|
||||
<div class="page">
|
||||
<%if(logged_in_user) {%>
|
||||
<div class="page-center">
|
||||
<div class="horizontal-button-container">
|
||||
<button onclick="deletePost()" href="#" class="button bad"><span>Delete Post</span></button>
|
||||
<button onclick="window.location = '/post/<%=blog_post.id %>/edit'" class="button caution"><span>Edit Post</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<%}%>
|
||||
|
||||
<div class="page-center">
|
||||
<div class="title"><%= blog_post.title%></div>
|
||||
<%- blog_post.content %>
|
||||
|
@ -17,3 +27,5 @@
|
|||
<%- include("partials/footer.ejs") %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script defer src="/js/post.js"></script>
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/newPost.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/generic.css" />
|
||||
<script src="/js/generic.js"></script>
|
||||
<title>Yet-Another-Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
<%- include("partials/header.ejs") %>
|
||||
<div class="page">
|
||||
<div class="page-center">
|
||||
<div class="page-title">New Post</div>
|
||||
<div class="info-container">
|
||||
<div class="title">Title</div>
|
||||
<input id="post-title" value="<%= existing_blog.title %>" type="text" />
|
||||
</div>
|
||||
<div class="info-container">
|
||||
<div class="title">Description</div>
|
||||
<textarea id="post-description"><%= existing_blog.description %></textarea>
|
||||
</div>
|
||||
<div class="info-container">
|
||||
<div class="title">Tags</div>
|
||||
<input id="post-tags" type="text" value="<%= existing_blog.tags %>" />
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="info-container">
|
||||
<div class="title">Content</div>
|
||||
<div class="rich-text-editor">
|
||||
<div class="controls">
|
||||
<div class="left">
|
||||
<a id="insert-h1"><span>H1</span></a>
|
||||
<a id="insert-h2"><span>H2</span></a>
|
||||
<a id="insert-h3"><span>H3</span></a>
|
||||
<a id="insert-h4"><span>H4</span></a>
|
||||
<a id="insert-underline">
|
||||
<span><u>U</u></span>
|
||||
</a>
|
||||
<a id="insert-italics">
|
||||
<span><i>i</i></span>
|
||||
</a>
|
||||
<a id="insert-bold">
|
||||
<span><strong>B</strong></span>
|
||||
</a>
|
||||
<a id="insert-strike">
|
||||
<span><del>S</del></span>
|
||||
</a>
|
||||
<a id="insert-sup">
|
||||
<span><sup>Sup</sup></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="right">
|
||||
<a id="insert-sidebyside"><img src="/img/textarea/sidebyside.svg" /></a>
|
||||
<a id="insert-video"><img src="/img/textarea/video.svg" /></a>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="post-content"><%= existing_blog.raw_content %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-container">
|
||||
<div class="title">Post Date</div>
|
||||
<div class="side-by-side-info">
|
||||
<input id="date" type="date" value="<%= existing_blog.publish_date %>" />
|
||||
<input id="time" type="time" value="<%= existing_blog.publish_time %>" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-container">
|
||||
<div class="side-by-side-info">
|
||||
<button class="button caution">Unlisted</button>
|
||||
<button onclick="publish()" class="button">Publish</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include("partials/footer.ejs") %>
|
||||
</body>
|
||||
</html>
|
||||
<script defer src="/js/newPost.js"></script>
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="32"
|
||||
viewBox="0 -960 800 640"
|
||||
width="40"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="sidebyside.svg"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="24.291667"
|
||||
inkscape:cx="20.027444"
|
||||
inkscape:cy="16.013722"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1368"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M 193,-320 0,-513 l 193,-193 42,42 -121,121 h 316 v 60 H 114 l 121,121 z m 414,-254 -42,-42 121,-121 H 370 v -60 h 316 l -121,-121 42,-42 193,193 z"
|
||||
id="path1"
|
||||
style="fill:#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="40"
|
||||
viewBox="0 -960 800 800"
|
||||
width="40"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="video.svg"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="24.291667"
|
||||
inkscape:cx="20.027444"
|
||||
inkscape:cy="20.048027"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1368"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M 303,-390 570,-560 303,-730 Z m 97,230 q -82,0 -155,-31.5 -73,-31.5 -127.5,-86 Q 63,-332 31.5,-405 0,-478 0,-560 q 0,-83 31.5,-156 31.5,-73 86,-127 54.5,-54 127.5,-85.5 73,-31.5 155,-31.5 83,0 156,31.5 73,31.5 127,85.5 54,54 85.5,127 31.5,73 31.5,156 0,82 -31.5,155 -31.5,73 -85.5,127.5 -54,54.5 -127,86 -73,31.5 -156,31.5 z m 0,-60 q 142,0 241,-99.5 99,-99.5 99,-240.5 0,-142 -99,-241 -99,-99 -241,-99 -141,0 -240.5,99 -99.5,99 -99.5,241 0,141 99.5,240.5 Q 259,-220 400,-220 Z m 0,-340 z"
|
||||
id="path1"
|
||||
style="fill:#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -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";
|
||||
});
|
|
@ -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() {}
|
15
yab.js
15
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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue