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
Armored Dragon 2024-04-14 18:52:16 -05:00
parent 5ab8a79e78
commit ca8b4ae5af
Signed by: ArmoredDragon
GPG Key ID: C7207ACC3382AD8B
19 changed files with 590 additions and 111 deletions

View File

@ -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 };

View File

@ -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,

View File

@ -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 };

View File

@ -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) });

View File

@ -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

View File

@ -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%;

View File

@ -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%;

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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";
});

View File

@ -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
View File

@ -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);