diff --git a/backend/core/core.js b/backend/core/core.js index 915bb5c..3557aaa 100644 --- a/backend/core/core.js +++ b/backend/core/core.js @@ -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 `
`; + }); + content = content.replace(image_regex, (match, image_name) => { for (image of images) { if (image.includes(image_name)) { return `
`; } } + + // 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); diff --git a/backend/form_validation.js b/backend/form_validation.js index 212110c..c7ffe93 100644 --- a/backend/form_validation.js +++ b/backend/form_validation.js @@ -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 }; diff --git a/backend/page_scripts.js b/backend/page_scripts.js index 1ef56f0..0be8c91 100644 --- a/backend/page_scripts.js +++ b/backend/page_scripts.js @@ -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) { diff --git a/frontend/public/css/blogNew.css b/frontend/public/css/blogNew.css index 6827db9..f1e8322 100644 --- a/frontend/public/css/blogNew.css +++ b/frontend/public/css/blogNew.css @@ -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 { diff --git a/frontend/public/css/blogNew.scss b/frontend/public/css/blogNew.scss index 7fa4689..6724bfe 100644 --- a/frontend/public/css/blogNew.scss +++ b/frontend/public/css/blogNew.scss @@ -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; } } diff --git a/frontend/public/css/blogSingle.css b/frontend/public/css/blogSingle.css index 0d4427c..23c5e97 100644 --- a/frontend/public/css/blogSingle.css +++ b/frontend/public/css/blogSingle.css @@ -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; } diff --git a/frontend/public/css/blogSingle.scss b/frontend/public/css/blogSingle.scss index 666c0ec..699f09d 100644 --- a/frontend/public/css/blogSingle.scss +++ b/frontend/public/css/blogSingle.scss @@ -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 { diff --git a/frontend/public/css/signin.css b/frontend/public/css/signin.css index fc463eb..efaf286 100644 --- a/frontend/public/css/signin.css +++ b/frontend/public/css/signin.css @@ -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; } \ No newline at end of file diff --git a/frontend/public/css/signin.scss b/frontend/public/css/signin.scss index dce8b34..a46469f 100644 --- a/frontend/public/css/signin.scss +++ b/frontend/public/css/signin.scss @@ -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; + } } } } diff --git a/frontend/public/img/textarea/sidebyside.svg b/frontend/public/img/textarea/sidebyside.svg new file mode 100644 index 0000000..406a71e --- /dev/null +++ b/frontend/public/img/textarea/sidebyside.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/public/img/textarea/video.svg b/frontend/public/img/textarea/video.svg new file mode 100644 index 0000000..11aec2c --- /dev/null +++ b/frontend/public/img/textarea/video.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/frontend/public/js/newBlog.js b/frontend/public/js/newBlog.js index 1ff8810..f25b54b 100644 --- a/frontend/public/js/newBlog.js +++ b/frontend/public/js/newBlog.js @@ -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(); diff --git a/frontend/views/blogNew.ejs b/frontend/views/blogNew.ejs index 54a2dc2..8270f29 100644 --- a/frontend/views/blogNew.ejs +++ b/frontend/views/blogNew.ejs @@ -38,6 +38,33 @@
+
+ +
+ + +
+
@@ -51,9 +78,9 @@ <% } %> <% if(existing_blog.publish_date) {%> - + <%} else { %> - + <% } %> diff --git a/frontend/views/login.ejs b/frontend/views/login.ejs index 6412a2f..3236416 100644 --- a/frontend/views/login.ejs +++ b/frontend/views/login.ejs @@ -23,8 +23,8 @@
- - Register + + Register
diff --git a/frontend/views/register.ejs b/frontend/views/register.ejs index 249543f..cccfff9 100644 --- a/frontend/views/register.ejs +++ b/frontend/views/register.ejs @@ -23,8 +23,8 @@
- - Login + + Login
diff --git a/package-lock.json b/package-lock.json index 4fff72f..4a911ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index a02d532..0223fd3 100644 --- a/package.json +++ b/package.json @@ -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",