Blog Post Improvements (#7)

Added:
+ In-line video embeds to YouTube, and Odysee.
+ Header anchors, allowing for linking to specific parts in a post
+ Custom text editor for ease of use for supported Markdown syntax

Fixed:
= Spacing on register / login page buttons
= Nonexisting blogs trying to be rendered instead of redirecting
= Image placeholder text not being displayed (being removed when not intended to)
= Images not being uploaded when creating a new blog post
= Undefined images being rendered as "undefined" text (Now does not render at all)

Reviewed-on: #7
Co-authored-by: Armored-Dragon <forgejo3829105@armoreddragon.com>
Co-committed-by: Armored-Dragon <forgejo3829105@armoreddragon.com>
pull/2/head
Armored Dragon 2023-11-29 08:45:22 +00:00 committed by Armored Dragon
parent 83da8100dc
commit 5ac2196d00
17 changed files with 536 additions and 26 deletions

View File

@ -11,7 +11,16 @@ const s3 = new S3Client({
region: process.env.S3_REGION,
endpoint: process.env.S3_ENDPOINT,
});
const md = require("markdown-it")();
const md = require("markdown-it")()
.use(require("markdown-it-underline"))
.use(require("markdown-it-footnote"))
.use(require("markdown-it-sup"))
.use(require("markdown-it-anchor"), {
permalink: require("markdown-it-anchor").permalink.linkInsideHeader({
placement: "before",
symbol: ``,
}),
});
let settings = {
SETUP_COMPLETE: false,
@ -79,8 +88,17 @@ async function postBlog(blog_post, owner_id) {
if (user.data.role !== "ADMIN" && user.data.role !== "AUTHOR") return { success: false, message: "User is not permitted" };
// Create object without image data to store in the database
let blog_post_formatted = {
title: blog_post.title,
description: blog_post.description,
content: blog_post.content,
visibility: blog_post.visibility,
publish_date: blog_post.publish_date,
};
// Save to database
const database_blog = await prisma.blogPost.create({ data: { ...blog_post, owner: { connect: { id: owner_id } } } });
const database_blog = await prisma.blogPost.create({ data: { ...blog_post_formatted, owner: { connect: { id: owner_id } } } });
// Init image vars
let uploaded_images = [];
@ -236,7 +254,7 @@ async function deleteImage(image, requester_id) {
const post = await getBlog({ id: image.parent, raw: true });
// Check if post exists
if (!post.success) return { success: false, message: "Post does not exist" };
if (!post) return { success: false, message: "Post does not exist" };
// Check for permissions
if (post.owner.id !== user.data.id || user.data.role !== "ADMIN") return { success: false, message: "User is not permitted" };
@ -350,12 +368,24 @@ function _format_blog_content(content, images) {
// Replace Side-by-side
const side_by_side = /{sidebyside}(.*?){\/sidebyside}/gs;
// Replace video links
const video = /{video:([^}]+)}/g;
content = content.replace(video, (match, inner_content) => {
return `<div class='video-embed'><iframe src="${_getVideoEmbed(
inner_content
)}" frameborder="0" allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
});
content = content.replace(image_regex, (match, image_name) => {
for (image of images) {
if (image.includes(image_name)) {
return `<div class='image-container'><img src='${image}'></div>`;
}
}
// Unknown image (Image was probably deleted)
return "";
});
content = content.replace(side_by_side, (match, inner_content) => {
@ -364,6 +394,24 @@ function _format_blog_content(content, images) {
// Finished formatting, return!
return content;
function _getVideoEmbed(video_url) {
// YouTube
if (video_url.includes("youtu.be")) {
return `https://youtube.com/embed/${video_url.split("/")[3]}`;
}
if (video_url.includes("youtube")) {
let video_id = video_url.split("/")[3];
video_id = video_id.split("watch?v=").pop();
return `https://youtube.com/embed/${video_id}`;
}
// Odysee
if (video_url.includes("://odysee.com")) {
let video_link = `https://${video_url.split("/")[2]}/$/embed/${video_url.split("/")[3]}/${video_url.split("/")[4]}`;
return video_link;
}
}
}
function _getNavigationList(current_page, max_page) {
current_page = Number(current_page);

View File

@ -32,6 +32,8 @@ async function postBlog(blog_object) {
content: blog_object.content,
visibility: blog_object.visibility,
publish_date: publish_date,
images: blog_object.images,
thumbnail: blog_object.thumbnail,
};
return { success: true, data: blog_post_formatted };

View File

@ -35,7 +35,7 @@ async function blogList(req, res) {
}
async function blogSingle(req, res) {
const blog = await core.getBlogList({ id: req.params.blog_id });
if (blog === null) return res.redirect("/blog");
if (blog.success === false) return res.redirect("/blog");
res.render("blogSingle.ejs", { ...getDefaults(req), blog_post: blog });
}
function blogNew(request, response) {

View File

@ -75,10 +75,59 @@
padding: 5px;
margin-bottom: 10px;
}
.e-content .text-actions {
height: 35px;
width: 100%;
background-color: #424242;
border-radius: 2px;
display: flex;
flex-flow: row wrap;
place-content: space-around;
}
.e-content .text-actions .left,
.e-content .text-actions .right {
height: 100%;
display: flex;
flex-direction: row;
}
.e-content .text-actions .left a,
.e-content .text-actions .right a {
height: 100%;
max-height: 100%;
min-width: 50px;
display: flex;
border-radius: 2px;
background-color: #333;
box-sizing: border-box;
cursor: pointer;
}
.e-content .text-actions .left a span,
.e-content .text-actions .right a span {
margin: auto;
}
.e-content .text-actions .left a img,
.e-content .text-actions .right a img {
margin: auto;
height: 100%;
padding: 5px;
box-sizing: border-box;
color: white;
}
.e-content .text-actions .left {
margin: 0 auto 0 0;
}
.e-content .text-actions .right {
margin: 0 0 0 auto;
}
.e-content .text-actions a:hover,
.e-content .text-actions a:focus {
filter: brightness(50%);
}
.e-content textarea {
font-size: 16px;
min-height: 200px;
color: white;
outline: 0;
}
.e-settings {

View File

@ -84,10 +84,64 @@ $background-body: #222;
background-color: $background-body;
padding: 5px;
margin-bottom: 10px;
.text-actions {
height: 35px;
width: 100%;
background-color: #424242;
border-radius: 2px;
display: flex;
flex-flow: row wrap;
place-content: space-around;
.left,
.right {
height: 100%;
display: flex;
flex-direction: row;
a {
height: 100%;
max-height: 100%;
min-width: 50px;
display: flex;
border-radius: 2px;
background-color: #333;
box-sizing: border-box;
cursor: pointer;
span {
margin: auto;
}
img {
margin: auto;
height: 100%;
padding: 5px;
box-sizing: border-box;
color: white;
}
}
}
.left {
margin: 0 auto 0 0;
}
.right {
margin: 0 0 0 auto;
}
a:hover,
a:focus {
filter: brightness(50%);
}
}
textarea {
font-size: 16px;
min-height: 200px;
color: white;
outline: 0;
}
}

View File

@ -11,6 +11,16 @@
.page .image-container img {
width: 100%;
}
.page .video-embed {
width: 100%;
min-height: 560px;
display: flex;
}
.page .video-embed iframe {
width: 100%;
height: 560px;
margin: auto;
}
.page .side-by-side {
display: flex;
flex-flow: row wrap;
@ -22,6 +32,16 @@
width: 50%;
margin-bottom: 0;
}
.page .side-by-side .video-embed {
width: 50%;
min-height: 280px;
padding: 5px;
box-sizing: border-box;
}
.page .side-by-side .video-embed iframe {
width: 100%;
height: 280px;
}
.page h1 {
border-bottom: 1px solid #777;
}

View File

@ -15,6 +15,18 @@
}
}
.video-embed {
width: 100%;
min-height: 560px;
display: flex;
iframe {
width: 100%;
height: 560px;
margin: auto;
}
}
.side-by-side {
display: flex;
flex-flow: row wrap;
@ -26,6 +38,18 @@
width: 50%;
margin-bottom: 0;
}
.video-embed {
width: 50%;
min-height: 280px;
padding: 5px;
box-sizing: border-box;
iframe {
width: 100%;
height: 280px;
}
}
}
h1 {

View File

@ -2,6 +2,7 @@ body {
margin: 0;
background-color: #111;
color: white;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.header {
@ -19,6 +20,9 @@ body {
font-size: 20px;
height: 100%;
}
.header .left a {
width: inherit;
}
.header .right {
margin: auto 0 auto auto;
height: 100%;
@ -55,6 +59,21 @@ button:focus {
background-color: #122d57;
}
button.good,
a.good {
background-color: #015b01;
}
button.yellow,
a.yellow {
background-color: #4a4a00;
}
button.bad,
a.bad {
background-color: #8e0000;
}
.page {
width: 1000px;
min-height: 10px;
@ -64,11 +83,89 @@ button:focus {
display: flex;
flex-direction: row;
}
.page .horizontal-button-container button {
width: 100px;
.page .horizontal-button-container button,
.page .horizontal-button-container a {
width: 120px;
min-height: 30px;
text-decoration: none;
display: flex;
margin-right: 5px;
}
.page .horizontal-button-container button span,
.page .horizontal-button-container a span {
margin: auto;
}
.page .horizontal-button-container button:last-of-type,
.page .horizontal-button-container a:last-of-type {
margin-right: 0;
}
.page .blog-admin {
margin-bottom: 10px;
}
.page .pagination {
display: flex;
flex-direction: row;
width: 100%;
margin: 0 auto;
}
.page .pagination a {
height: 40px;
width: 150px;
margin-right: 5px;
display: flex;
text-decoration: none;
background-color: #222;
}
.page .pagination a span {
margin: auto;
color: white;
font-size: 20px;
}
.page .pagination a:last-of-type {
margin-right: 0;
margin-left: 5px;
}
.page .pagination a.disabled {
filter: brightness(50%);
}
.page .pagination .page-list {
flex-grow: 1;
display: flex;
flex-direction: row;
margin-bottom: 50px;
}
.page .pagination .page-list a {
width: 40px;
height: 40px;
display: flex;
text-decoration: none;
background-color: #222;
border-radius: 10px;
margin: 0 10px 0 0;
}
.page .pagination .page-list a span {
margin: auto;
color: white;
}
.page .pagination .page-list a:first-of-type {
margin: auto 10px auto auto;
}
.page .pagination .page-list a:last-of-type {
margin: auto auto auto 0;
}
.page .pagination .page-list a.active {
background-color: #993d00;
}
.hidden {
display: none !important;
}
@media screen and (max-width: 1010px) {
.page {
width: 95%;
}
}
.center-modal {
margin: auto;
width: 400px;
@ -103,10 +200,18 @@ button:focus {
.center-modal .horizontal-button-container {
flex-direction: row-reverse !important;
}
.center-modal .horizontal-button-container a {
color: white;
margin: auto auto auto 0;
.center-modal .horizontal-button-container * {
width: 100% !important;
}
.center-modal .horizontal-button-container a,
.center-modal .horizontal-button-container button {
width: 200px !important;
color: white;
display: flex;
width: -moz-min-content;
width: min-content;
}
.center-modal .horizontal-button-container a span,
.center-modal .horizontal-button-container button span {
margin: auto;
text-align: center;
}

View File

@ -37,13 +37,21 @@
.horizontal-button-container {
flex-direction: row-reverse !important;
a {
color: white;
margin: auto auto auto 0;
* {
width: 100% !important;
}
a,
button {
width: 200px !important;
color: white;
display: flex;
width: min-content;
span {
margin: auto;
text-align: center;
}
}
}
}

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.1 (91b66b0783, 2023-11-16, 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.006861"
inkscape:cy="16.013722"
inkscape:window-width="2560"
inkscape:window-height="1370"
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:#f9f9f9" />
</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.1 (91b66b0783, 2023-11-16, 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.006861"
inkscape:cy="20.006861"
inkscape:window-width="2560"
inkscape:window-height="1370"
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:#f9f9f9" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -26,9 +26,14 @@ function stylizeDropArea(element) {
});
}
// Auto resize on page load
text_area.style.height = text_area.scrollHeight + "px";
text_area.style.minHeight = text_area.scrollHeight + "px";
// Auto expand blog area
qs(".e-content textarea").addEventListener("input", (e) => {
e.target.style.height = e.target.scrollHeight + "px";
text_area.addEventListener("input", (e) => {
text_area.style.height = text_area.scrollHeight + "px";
text_area.style.minHeight = e.target.scrollHeight + "px";
});
stylizeDropArea(thumbnail_area);
@ -163,7 +168,7 @@ function updateImages() {
qsa(".e-image-area .image").forEach((entry) => entry.remove());
// Clear placeholder text
if (qs(".e-image-area .placeholder")) qs(".e-image-area .placeholder").remove();
if (existing_images.length + pending_images.length > 0) if (qs(".e-image-area .placeholder")) qs(".e-image-area .placeholder").remove();
existing_images.forEach((image) => {
image_area.insertAdjacentHTML("beforeend", image_div(image.id, image.url));
@ -177,6 +182,44 @@ function updateImages() {
customDragString();
}
// 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 = text_area.selectionStart;
const selectionEnd = text_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);
let updatedText;
if (dual_side) {
updatedText = `${textBefore}${insert}${selectedText}${insert}${textAfter}`;
} else {
updatedText = `${textBefore}${insert}${selectedText}${textAfter}`;
}
text_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);
}
text_area.addEventListener("drop", (event) => {
event.preventDefault();

View File

@ -38,6 +38,33 @@
</div>
<div class="e-content">
<div class="text-actions">
<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="content" placeholder="Tell us about your subject..."><%= existing_blog.raw_content %></textarea>
</div>
@ -51,9 +78,9 @@
<% } %>
<!-- -->
<% if(existing_blog.publish_date) {%>
<input id="time" type="time" step="3600" value="<%= existing_blog.publish_time %>" />
<input id="time" type="time" value="<%= existing_blog.publish_time %>" />
<%} else { %>
<input id="time" type="time" value="13:00" step="3600" />
<input id="time" type="time" value="13:00" />
<% } %>
</div>
</div>

View File

@ -23,8 +23,8 @@
<input id="password" type="password" />
</div>
<div class="horizontal-button-container">
<button onclick="requestLogin()"><div>Login</div></button>
<a href="/register">Register</a>
<button onclick="requestLogin()"><span>Login</span></button>
<a href="/register"><span>Register</span></a>
</div>
</div>
</div>

View File

@ -23,8 +23,8 @@
<input id="password" type="password" />
</div>
<div class="horizontal-button-container">
<button onclick="requestRegister()"><div>Register</div></button>
<a href="/login">Login</a>
<button onclick="requestRegister()"><span>Register</span></button>
<a href="/login"><span>Login</span></a>
</div>
</div>
</div>

56
package-lock.json generated
View File

@ -20,6 +20,10 @@
"express-session": "^1.17.3",
"feed": "^4.2.2",
"markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.7",
"markdown-it-footnote": "^3.0.3",
"markdown-it-sup": "^1.0.0",
"markdown-it-underline": "^1.0.1",
"multer": "^1.4.5-lts.1",
"prisma": "^5.6.0",
"sharp": "^0.32.5",
@ -1495,6 +1499,28 @@
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="
},
"node_modules/@types/linkify-it": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
"integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
"peer": true
},
"node_modules/@types/markdown-it": {
"version": "13.0.7",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz",
"integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==",
"peer": true,
"dependencies": {
"@types/linkify-it": "*",
"@types/mdurl": "*"
}
},
"node_modules/@types/mdurl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
"integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
"peer": true
},
"node_modules/@types/node": {
"version": "20.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz",
@ -2854,6 +2880,30 @@
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/markdown-it-anchor": {
"version": "8.6.7",
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
"peerDependencies": {
"@types/markdown-it": "*",
"markdown-it": "*"
}
},
"node_modules/markdown-it-footnote": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz",
"integrity": "sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w=="
},
"node_modules/markdown-it-sup": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz",
"integrity": "sha512-E32m0nV9iyhRR7CrhnzL5msqic7rL1juWre6TQNxsnApg7Uf+F97JOKxUijg5YwXz86lZ0mqfOnutoryyNdntQ=="
},
"node_modules/markdown-it-underline": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/markdown-it-underline/-/markdown-it-underline-1.0.1.tgz",
"integrity": "sha512-J597ni39vPHIH1ONVZoDvQKUUXkOqoB93bm6Fc/5Deu6XaWMXrT0xf2m2r9qZCA8dncWJ5V8d5PyGkpmQuy/vg=="
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
@ -3524,9 +3574,9 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/sharp": {
"version": "0.32.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz",
"integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==",
"version": "0.32.6",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",

View File

@ -33,6 +33,10 @@
"express-session": "^1.17.3",
"feed": "^4.2.2",
"markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.7",
"markdown-it-footnote": "^3.0.3",
"markdown-it-sup": "^1.0.0",
"markdown-it-underline": "^1.0.1",
"multer": "^1.4.5-lts.1",
"prisma": "^5.6.0",
"sharp": "^0.32.5",