From 6df5d7e818c56a607bf775ecc2fe8ba235be5f46 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Thu, 18 Apr 2024 00:49:23 -0500 Subject: [PATCH] Author Page. Edit author page. Author display name. Generic media uploads. Core refactoring. Signed-off-by: Armored Dragon --- backend/core/core.js | 513 ++++++++++-------- backend/core/external_api.js | 2 +- backend/core/internal_api.js | 19 +- backend/form_validation.js | 2 +- backend/page_scripts.js | 41 +- frontend/views/themes/default/css/author.css | 25 +- frontend/views/themes/default/css/author.scss | 26 +- frontend/views/themes/default/css/generic.css | 55 ++ .../views/themes/default/css/generic.scss | 65 +++ frontend/views/themes/default/css/newPost.css | 49 -- .../views/themes/default/css/newPost.scss | 59 -- .../views/themes/default/css/settings.css | 4 + .../views/themes/default/css/settings.scss | 5 + frontend/views/themes/default/ejs/author.ejs | 19 +- .../views/themes/default/ejs/authorEdit.ejs | 43 ++ .../themes/default/ejs/partials/post.ejs | 2 +- .../default/ejs/partials/richTextEditor.ejs | 31 ++ frontend/views/themes/default/ejs/post.ejs | 2 +- frontend/views/themes/default/ejs/postNew.ejs | 36 +- .../views/themes/default/js/editAuthor.js | 12 + frontend/views/themes/default/js/newPost.js | 90 +-- .../views/themes/default/js/richTextEditor.js | 98 ++++ prisma/schema.prisma | 33 +- yab.js | 3 + 24 files changed, 744 insertions(+), 490 deletions(-) create mode 100644 frontend/views/themes/default/ejs/authorEdit.ejs create mode 100644 frontend/views/themes/default/ejs/partials/richTextEditor.ejs create mode 100644 frontend/views/themes/default/js/editAuthor.js create mode 100644 frontend/views/themes/default/js/richTextEditor.js diff --git a/backend/core/core.js b/backend/core/core.js index f4a84a0..8c46a9a 100644 --- a/backend/core/core.js +++ b/backend/core/core.js @@ -57,23 +57,20 @@ function _initS3Storage() { } } -async function registerUser(username, password, options) { - let user_database_entry; - let user_profile_database_entry; +// Users +async function newUser({ username, password, role } = {}) { + if (!username) return _r(false, "Username not specified"); + if (!password) return _r(false, "Password not specified"); - // Create the entry in the database + // Create the account try { - user_database_entry = await prisma.user.create({ data: { username: username, password: password, ...options } }); + user_database_entry = await prisma.user.create({ data: { username: username, password: password, role: role } }); } catch (e) { - let message; - - if (e.code === "P2002") message = "Username already exists"; - else message = "Unknown error"; - + let message = "Unknown error"; return { success: false, message: message }; } - // Create a user profile page + // Create the profile page and link try { user_profile_database_entry = await prisma.profilePage.create({ data: { owner: { connect: { id: user_database_entry.id } } } }); } catch (e) { @@ -81,27 +78,68 @@ async function registerUser(username, password, options) { } // Master user was created; server initialized - postSetting("SETUP_COMPLETE", true); - - // User has been successfully created - return { success: true, message: `Successfully created ${username}` }; + editSetting({ name: "SETUP_COMPLETE", value: true }); } +async function getUser({ user_id, username }) { + if (!username && !user_id) return _r(false, "Either a user_id or username is needed."); + + let user; + + if (user_id) user = await prisma.user.findUnique({ where: { id: user_id } }); + else if (username) user = await prisma.user.findUnique({ where: { username: username } }); + + if (!user) return _r(false, "No matching user"); + else return { success: true, data: user }; +} +async function editUser({ requester_id, user_id, user_content }) { + let user = await getUser({ user_id: user_id }); + if (!user.success) return _r(false, "User not found"); + user = user.data; + + // TODO: + // If there was a role change, see if the acting user can make these changes + + // TODO: + // If there was a password change, + // check to see if the user can make these changes + // Hash the password + + // FIXME: Not secure. ASAP! + let formatted = {}; + formatted[user_content.setting_name] = user_content.value; + + await prisma.user.update({ where: { id: user.id }, data: formatted }); + return _r(true); +} +async function deleteUser({ user_id }) { + if (!user_id) return _r(false, "User_id not specified."); + + await prisma.user.delete({ where: { id: user_id } }); // TODO: Test + return _r(true, `User ${user_id} deleted`); +} + // Posts -async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, page = 0, search_title = false, search_content = false, search_tags = false, search }) { - // If we have an ID, we want a single post - if (id) { - // Get the post by the id - let post = await prisma.blogPost.findUnique({ where: { id: id }, include: { owner: true } }); - if (!post) return { success: false, message: "Post does not exist" }; +async function newPost({ requester_id }) { + // const user = await getUser({ id: requester_id }); + const post = await prisma.post.create({ data: { owner: { connect: { id: requester_id } } } }); - // Render the post - const rendered_post = await _renderPost(post, true); - - // Return the post with valid image urls - return { data: rendered_post, success: true }; + // TODO: Validate request (Does user have perms?) + // TODO: Does server allow new posts? + return post.id; +} +async function getPost({ requester_id, post_id, visibility = "PUBLISHED" } = {}, { search, search_title, search_content, search_tags } = {}, { limit = 10, page = 0, pagination = true } = {}) { + // Get a single post + if (post_id) { + let post; + post = await prisma.post.findUnique({ where: { id: post_id }, include: { owner: true } }); + if (!post) return _r(false, "Post does not exist"); + post = _stripPrivatePost(post); + // Render post + return { success: true, data: await _renderPost(post) }; } + // Otherwise build WHERE_OBJECT using data we do have - let rendered_post_list = []; + let post_list = []; let where_object = { OR: [ // Standard discovery: Public, and after the publish date @@ -120,7 +158,7 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag // User owns the post { - ownerid: owner_id, + ownerid: requester_id, }, ], @@ -130,16 +168,14 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag }, ], }; - // Build the "where_object" object if (search) { if (search_tags) where_object["AND"][0]["OR"].push({ tags: { hasSome: [search?.toLowerCase()] } }); if (search_title) where_object["AND"][0]["OR"].push({ title: { contains: search, mode: "insensitive" } }); if (search_content) where_object["AND"][0]["OR"].push({ content: { contains: search, mode: "insensitive" } }); } - // Execute search - const blog_posts = await prisma.blogPost.findMany({ + let posts = await prisma.post.findMany({ where: where_object, take: limit, skip: Math.max(page, 0) * limit, @@ -147,59 +183,116 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag orderBy: [{ publish_date: "desc" }, { created_date: "desc" }], }); - // Render each of the posts in the list - for (post of blog_posts) { - rendered_post_list.push(await _renderPost(post, true)); + for (post of posts) { + post = _stripPrivatePost(post); + post = await _renderPost(post); + post_list.push(post); } + // Calculate pagination - let pagination = await prisma.blogPost.count({ + let post_count = await prisma.post.count({ where: where_object, }); - return { data: rendered_post_list, pagination: _getNavigationList(page, Math.ceil(pagination / limit)), success: true }; + + return { data: post_list, pagination: _getNavigationList(page, Math.ceil(post_count / limit)), success: true }; + + function _getNavigationList(current_page, max_page) { + current_page = Number(current_page); + max_page = Number(max_page); + + const pageList = [current_page - 2, current_page - 1, current_page, current_page + 1, current_page + 2].filter((num) => num >= 0 && num < max_page); + return pageList.slice(0, 5); + } } -async function getAuthorPage({ author_id }) { - // Get the post by the id +async function editPost({ requester_id, post_id, post_content }) { + let user = await getUser({ user_id: requester_id }); + let post = await getPost({ post_id: post_id }); + let publish_date = null; - let post = await prisma.profilePage.findUnique({ where: { ownerid: author_id }, include: { owner: true } }); - if (!post) return { success: false, message: "Post does not exist" }; + if (!user.success) return _r(false, post.message || "User not found"); + user = user.data; + if (!post.success) return _r(false, post.message || "Post not found"); + post = post.data; - // Render the post - const rendered_post = await _renderPost(post, true); + // Check to see if the requester can update the post + // TODO: Permissions + let can_update = post.owner.id === user.id || user.role === "ADMIN"; - // Return the post with valid image urls - return { data: rendered_post, success: true }; -} -async function getUser({ id, username } = {}) { - let user; - if (id) user = await prisma.user.findUnique({ where: { id: id } }); - else if (username) user = await prisma.user.findUnique({ where: { username: username } }); + // FIXME: Unsure if this actually works + // Check if we already have a formatted publish date + if (typeof post.publish_date !== "object") { + const [year, month, day] = post.date.split("-"); + const [hour, minute] = post.time.split(":"); + publish_date = new Date(year, month - 1, day, hour, minute); + } - if (!user) return { success: false, message: "No matching user" }; - else return { success: true, data: user }; -} -async function postBlog(blog_post, owner_id) { - const user = await getUser({ id: owner_id }); - // Check if user has permissions to upload a blog post - - 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, - tags: blog_post.tags, + // Rebuild the post to save + let post_formatted = { + title: post_content.title, + description: post_content.description, + content: post_content.content, + visibility: post_content.visibility || "PRIVATE", + publish_date: publish_date || post_content.publish_date, + tags: post_content.tags, + media: [...post.raw_media, ...post_content.media], }; - // Save to database - const database_blog = await prisma.blogPost.create({ data: { ...blog_post_formatted, owner: { connect: { id: owner_id } } } }); + // Save the updated post to the database + await prisma.post.update({ where: { id: post.id }, data: post_formatted }); - // 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 }; + return _r(true); } +async function deletePost({ requester_id, post_id }) {} +// User Profiles +async function getBiography({ requester_id, author_id }) { + if (!author_id) return _r(false, "No Author specified."); + let post = await prisma.profilePage.findFirst({ where: { ownerid: author_id }, include: { owner: true } }); + + // Check if it is private + // TODO + + // HACK: + // When we render the post and reading from S3, we want the post id + // The problem is when a user views the biography page, the page shows the account id opposed to the "profile page" id. + // This causes a incorrect parent_id value and an incorrect key. + // Replace the "id" to the value it's expecting. + const original_post_id = post.id; + let rendering_formatted_post = {}; + + rendering_formatted_post = post; + rendering_formatted_post.id = author_id; + + // Render + post = _stripPrivatePost(post); + post = await _renderPost(rendering_formatted_post); + + post.id = original_post_id; + + return { success: true, data: post }; +} +async function updateBiography({ requester_id, author_id, biography_content }) { + let user = await getUser({ user_id: requester_id }); + let biography = await getBiography({ author_id: author_id }); + + if (!user.success) return _r(false, user.message || "Author not found"); + user = user.data; + + if (!biography.success) return _r(false, biography.message || "Post not found"); + biography = biography.data; + + let can_update = biography.owner.id === user.id || user.role === "ADMIN"; + if (!can_update) return _r(false, "User not permitted"); + + let formatted = { + content: biography_content.content, + media: [...biography.raw_media, ...biography_content.media], + }; + + await prisma.profilePage.update({ where: { id: biography.id }, data: formatted }); + + return _r(true); +} +// TODO: Replace async function deleteBlog(blog_id, requester_id) { const user = await getUser({ id: requester_id }); const post = await getBlog({ id: blog_id }); @@ -209,85 +302,30 @@ async function deleteBlog(blog_id, requester_id) { let can_delete = post.data.owner.id === user.data.id || user.data.role === "ADMIN"; if (can_delete) { - await prisma.blogPost.delete({ where: { id: post.data.id } }); + await prisma.post.delete({ where: { id: post.data.id } }); _deleteS3Directory(post.data.id, "blog"); return { success: true }; } return { success: false, message: "Action not permitted" }; } -async function updateBlog(blog_post, requester_id) { - const user = await getUser({ id: requester_id }); - const post = await getBlog({ id: blog_post.id, raw: true }); - let publish_date = null; - - delete blog_post.id; - - if (!post.success) return { success: false, message: post.message || "Post not found" }; - - let can_update = post.data.owner.id === user.data.id || user.data.role === "ADMIN"; - - if (!can_update) return { success: false, message: "User not permitted" }; - - // FIXME: Unsure if this actually works - // Check if we already have a formatted publish date - if (typeof blog_post.publish_date !== "object") { - const [year, month, day] = blog_post.date.split("-"); - 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, - content: blog_post.content, - 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 }); - - return { success: true }; -} -async function deleteImage(image, requester_id) { - const user = await getUser({ id: requester_id }); - const post = await getBlog({ id: image.parent, raw: true }); - - // Check if post exists - 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" }; - - let image_index = post.raw_images.indexOf(image.id); - - post.raw_images.splice(image_index, 1); - - await prisma.blogPost.update({ where: { id: post.id }, data: { images: post.raw_images } }); - - const request_params = { - Bucket: process.env.S3_BUCKET_NAME, - Key: `${process.env.ENVIRONMENT}/${image.parent_type}/${image.parent}/${image.id}.webp`, - }; - - const command = new DeleteObjectCommand(request_params); - await s3.send(command); - - return { success: true }; -} -async function postImage(post_id, buffer) { +async function uploadMedia({ parent_id, file_buffer, file_extension }) { if (!use_s3_storage) return null; - let size = { width: 1920, height: 1080 }; - const image_name = crypto.randomUUID(); + const content_name = crypto.randomUUID(); + let maximum_image_resolution = { width: 1920, height: 1080 }; - const compressed_image = await sharp(Buffer.from(buffer.split(",")[1], "base64"), { animated: true }) - .resize({ ...size, withoutEnlargement: true, fit: "inside" }) + // const image_extensions = ["png", "webp", "jpg", "jpeg"]; + // const video_extensions = ["mp4", "webm", "mkv", "avi"]; + + // Images + const compressed_image = await sharp(Buffer.from(file_buffer.split(",")[1], "base64"), { animated: true }) + .resize({ ...maximum_image_resolution, withoutEnlargement: true, fit: "inside" }) .webp({ quality: 90, animated: true }) .toBuffer(); + const params = { Bucket: process.env.S3_BUCKET_NAME, - Key: `${process.env.ENVIRONMENT}/posts/${post_id}/${image_name}.webp`, + Key: `${process.env.ENVIRONMENT}/posts/${parent_id}/${content_name}.webp`, Body: compressed_image, ContentType: "image/webp", }; @@ -295,20 +333,45 @@ async function postImage(post_id, buffer) { const command = new PutObjectCommand(params); await s3.send(command); - return image_name; + return content_name; } -async function _getImage(parent_id, parent_type, name) { +async function getMedia({ parent_id, file_name }) { if (!use_s3_storage) return null; - let params; - // Default image - if (name === "DEFAULT") params = { Bucket: process.env.S3_BUCKET_NAME, Key: `defaults/thumbnail.webp` }; - // Named image - else params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${name}.webp` }; - + const params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/posts/${parent_id}/${file_name}.webp` }; return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 }); } +// TODO: +// Will be done automatically in the background. +// Unreferenced images and media will be deleted +async function deleteMedia({ parent_id, file_name }) {} + +// async function deleteImage(image, requester_id) { +// const user = await getUser({ id: requester_id }); +// const post = await getBlog({ id: image.parent, raw: true }); + +// // Check if post exists +// 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" }; + +// let image_index = post.raw_images.indexOf(image.id); + +// post.raw_images.splice(image_index, 1); + +// await prisma.post.update({ where: { id: post.id }, data: { images: post.raw_images } }); + +// const request_params = { +// Bucket: process.env.S3_BUCKET_NAME, +// Key: `${process.env.ENVIRONMENT}/${image.parent_type}/${image.parent}/${image.id}.webp`, +// }; + +// const command = new DeleteObjectCommand(request_params); +// await s3.send(command); + +// return { success: true }; +// } async function _deleteS3Directory(id, type) { - // logger.verbose(`Deleting entire S3 image directory`); // Erase database images from S3 server const folder_params = { Bucket: process.env.S3_BUCKET_NAME, Prefix: `${process.env.ENVIRONMENT}/${type}/${id}` }; @@ -335,89 +398,82 @@ 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) { - if (raw) { - // Had to do this, only God knows why. - blog_post.raw_images = []; - if (blog_post.images) blog_post.images.forEach((image) => blog_post.raw_images.push(image)); - 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, "posts", blog_post.images[i]); +async function _renderPost(post) { + post.raw_media = []; + post.raw_content = post.content; + + // For some reason Node does not like to set a variable and leave it. + post.media.forEach((media) => post.raw_media.push(media)); + + if (post.media) { + for (i = 0; post.media.length > i; i++) { + post.media[i] = await getMedia({ parent_id: post.id, file_name: post.media[i] }); } } - if (blog_post.content) { + if (post.content) { // Render the markdown contents of the post - blog_post.content = md.render(blog_post.content); + post.content = md.render(post.content); // Replace custom formatting with what we want - blog_post.content = _format_blog_content(blog_post.content, blog_post.images); + post.content = _formatBlogContent(post.content, post.media); } + return post; - return blog_post; -} -function _format_blog_content(content, images) { - // Replace Images - const image_regex = /{image:([^}]+)}/g; + function _formatBlogContent(content, media_list) { + // Replace Images + const image_regex = /{image:([^}]+)}/g; - // Replace Side-by-side - const side_by_side = /{sidebyside}(.*?){\/sidebyside}/gs; + // Replace Side-by-side + const side_by_side = /{sidebyside}(.*?){\/sidebyside}/gs; - // Replace video links - const video = /{video:([^}]+)}/g; + // Replace video links + const video = /{video:([^}]+)}/g; - content = content.replace(video, (match, inner_content) => { - return `
`; - }); + 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 `
`; + // Replace Images + content = content.replace(image_regex, (match, image_name) => { + for (media of media_list) { + if (media.includes(image_name)) { + return `
`; + } + } + + // Unknown image (Image was probably deleted) + return ""; + }); + + content = content.replace(side_by_side, (match, inner_content) => { + return `
${inner_content}
`; + }); + + // 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; } } - - // Unknown image (Image was probably deleted) - return ""; - }); - - content = content.replace(side_by_side, (match, inner_content) => { - return `
${inner_content}
`; - }); - - // 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); - max_page = Number(max_page); - const pageList = [current_page - 2, current_page - 1, current_page, current_page + 1, current_page + 2].filter((num) => num >= 0 && num < max_page); - return pageList.slice(0, 5); -} async function _getSettings() { // Go though each object key in our settings to get the value if it exists Object.keys(settings).forEach(async (key) => { @@ -435,13 +491,7 @@ 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; -} - +// TODO: Replace async function getSetting(key, { parse = true }) { if (!settings[key]) return null; @@ -450,6 +500,7 @@ async function getSetting(key, { parse = true }) { } return settings[key]; } +// TODO: Replace async function postSetting(key, value) { try { if (!Object.keys(settings).includes(key)) return { success: false, message: "Setting not valid" }; @@ -466,5 +517,27 @@ async function postSetting(key, value) { return { success: false, message: e.message }; } } +// TODO: Replace +async function editSetting({ name, value }) { + if (!Object.keys(settings).includes(name)) return _r(false, "Setting is not valid"); -module.exports = { settings, newPost, registerUser, getUser, getAuthorPage, postBlog, updateBlog, getBlog, deleteBlog, postImage, deleteImage, postSetting, getSetting }; + await prisma.setting.upsert({ where: { id: key }, update: { value: value }, create: { id: key, value: value } }); + try { + settings[key] = JSON.parse(value); + } catch { + settings[key] = value; + } + + return _r(true); +} + +function _stripPrivatePost(post) { + if (!post) return; + if (post.owner) delete post.owner.password; + return post; +} +const _r = (s, m) => { + return { success: s, message: m }; +}; + +module.exports = { settings, newUser, getUser, editUser, getPost, newPost, editPost, getBiography, updateBiography, uploadMedia, deleteBlog, postSetting, getSetting }; diff --git a/backend/core/external_api.js b/backend/core/external_api.js index 83f61c2..986c085 100644 --- a/backend/core/external_api.js +++ b/backend/core/external_api.js @@ -23,7 +23,7 @@ async function getFeed({ type = "rss" }) { let feed = getBaseFeed(); // Get posts - let posts = await core.getBlog({ limit: 20 }); // internal.getBlogList({}, { limit: 20 }); + let posts = await core.getPost(null, null, { limit: 20 }); // For each post, add a formatted object to the feed posts.data.forEach((post) => { diff --git a/backend/core/internal_api.js b/backend/core/internal_api.js index 597991e..5ecc1d9 100644 --- a/backend/core/internal_api.js +++ b/backend/core/internal_api.js @@ -18,7 +18,7 @@ async function postRegister(req, res) { const role = core.settings["SETUP_COMPLETE"] ? undefined : "ADMIN"; const hashed_password = await bcrypt.hash(password, 10); // Hash the password for security :^) - res.json(await core.registerUser(username, hashed_password, { role: role })); + res.json(await core.newUser({ username: username, password: hashed_password, role: role })); } async function postLogin(req, res) { const { username, password } = req.body; // Get the username and password from the request body @@ -46,7 +46,7 @@ async function postSetting(request, response) { 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)); + return response.json(await core.uploadMedia({ parent_id: request.body.post_id, file_buffer: request.body.buffer })); } async function deleteImage(req, res) { // TODO: Permissions for deleting image @@ -78,10 +78,19 @@ async function patchBlog(req, res) { // User is admin, or user is author // Validate blog info - const valid = await validate.postBlog(req.body); + let valid = await validate.postBlog(req.body); + if (!valid.success) return { success: false, message: valid.message || "Post failed validation" }; + valid = valid.data; // TODO: Permissions for updating blog - return res.json(await core.updateBlog({ ...valid.data, id: req.body.id }, req.session.user.id)); + return res.json(await core.editPost({ requester_id: req.session.user.id, post_id: req.body.id, post_content: valid })); +} +async function patchBiography(request, response) { + // TODO: Validate + return response.json(await core.updateBiography({ requester_id: request.session.user.id, author_id: request.body.id, biography_content: request.body })); +} +async function patchUser(request, response) { + return response.json(await core.editUser({ requester_id: request.session.user.id, user_id: request.body.id, user_content: request.body })); } -module.exports = { postRegister, postLogin, postSetting, postImage, deleteImage, postBlog, deleteBlog, patchBlog }; +module.exports = { postRegister, patchBiography, postLogin, postSetting, postImage, deleteImage, postBlog, deleteBlog, patchBlog, patchUser }; diff --git a/backend/form_validation.js b/backend/form_validation.js index e65c501..de46b88 100644 --- a/backend/form_validation.js +++ b/backend/form_validation.js @@ -46,7 +46,7 @@ async function postBlog(blog_object) { visibility: blog_object.visibility, publish_date: publish_date, tags: valid_tag_array, - images: blog_object.images, + media: blog_object.media, thumbnail: blog_object.thumbnail, }; diff --git a/backend/page_scripts.js b/backend/page_scripts.js index 43068af..c0d84b8 100644 --- a/backend/page_scripts.js +++ b/backend/page_scripts.js @@ -13,7 +13,14 @@ async function index(request, response) { // const is_setup_complete = core.settings["SETUP_COMPLETE"]; // if (!is_setup_complete) return response.redirect("/register"); - const blog_list = await core.getBlog({ owner_id: request.session.user?.id, page: request.query.page || 0 }); + const blog_list = await core.getPost({ requester_id: request.session.user?.id, page: request.query.page || 0 }); + + blog_list.data.forEach((post) => { + let published_date_parts = new Date(post.publish_date).toLocaleDateString().split("/"); + const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`; + post.publish_date = formatted_date; + }); + response.render(getThemePage("index"), { ...getDefaults(request), blog_list: blog_list.data, @@ -29,15 +36,30 @@ function login(request, response) { response.render(getThemePage("login"), getDefaults(request)); } async function author(req, res) { - const user = await core.getUser({ id: req.params.author_id }); + const user = await core.getUser({ user_id: req.params.author_id }); // FIXME: Bandage fix for author get error if (!user.success) return res.redirect("/"); - const profile = await core.getAuthorPage({ author_id: user.data.id }); - console.log(profile.data); - res.render(getThemePage("author"), { ...getDefaults(req), post: profile.data }); + const profile = await core.getBiography({ author_id: user.data.id }); + // TODO: Check for success + // const posts = await core.getBlog({ owner_id: user.data.id, raw: true }); + const posts = await core.getPost({ requester_id: user.data.id }); + + res.render(getThemePage("author"), { ...getDefaults(req), post: { ...profile.data, post_count: posts.data.length } }); +} +async function authorEdit(request, response) { + let author = await core.getBiography({ author_id: request.params.author_id }); + if (!author.success) return response.redirect("/"); + response.render(getThemePage("authorEdit"), { ...getDefaults(request), profile: author.data }); } async function blogList(req, res) { - const blog_list = await core.getBlog({ owner_id: req.session.user?.id, page: req.query.page || 0, search: req.query.search, search_tags: true, search_title: true }); + const blog_list = await core.getPost({ requester_id: req.session.user?.id }, { search: req.query.search, search_title: true }); + + blog_list.data.forEach((post) => { + let published_date_parts = new Date(post.publish_date).toLocaleDateString().split("/"); + const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`; + post.publish_date = formatted_date; + }); + res.render(getThemePage("postSearch"), { ...getDefaults(req), blog_list: blog_list.data, @@ -47,16 +69,16 @@ async function blogList(req, res) { }); } async function blogSingle(req, res) { - const blog = await core.getBlog({ id: req.params.blog_id }); + const blog = await core.getPost({ post_id: req.params.blog_id }); if (blog.success === false) return res.redirect("/"); res.render(getThemePage("post"), { ...getDefaults(req), blog_post: blog.data }); } async function blogNew(request, response) { - const new_post = await core.newPost(request.session.user.id); + const new_post = await core.newPost({ requester_id: 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 }); + let existing_blog = await core.getPost({ post_id: req.params.blog_id }); if (existing_blog.success) existing_blog = existing_blog.data; // FIXME: Quickfix for .success/.data issue let published_time_parts = new Date(existing_blog.publish_date).toLocaleTimeString([], { timeStyle: "short" }).slice(0, 4).split(":"); @@ -94,4 +116,5 @@ module.exports = { admin, atom, jsonFeed, + authorEdit, }; diff --git a/frontend/views/themes/default/css/author.css b/frontend/views/themes/default/css/author.css index 6307c3d..dafda4b 100644 --- a/frontend/views/themes/default/css/author.css +++ b/frontend/views/themes/default/css/author.css @@ -2,7 +2,14 @@ display: flex; flex-direction: row; min-height: 50px; - margin-top: 4rem; + background-color: white; + 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; + border-radius: 5px; } .page .page-center .biography { width: 66.6666666667%; @@ -12,14 +19,22 @@ padding: 1rem; box-sizing: border-box; } +.page .page-center .biography .image-container { + max-width: 100%; +} +.page .page-center .biography .image-container img { + max-width: 100%; +} .page .page-center .about { width: 33.3333333333%; background-color: white; min-height: 50px; - margin: 0 1rem; + margin: 0 0 0 1rem; padding: 1rem; box-sizing: border-box; box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px; + height: -moz-fit-content; + height: fit-content; } .page .page-center .about .profile-picture { margin: auto auto 1.5rem auto; @@ -49,4 +64,10 @@ text-decoration: none; color: black; margin: auto; +} + +.page .nobackground { + background-color: transparent; + box-shadow: none; + padding: 0; } \ No newline at end of file diff --git a/frontend/views/themes/default/css/author.scss b/frontend/views/themes/default/css/author.scss index 0264b3d..a66b16f 100644 --- a/frontend/views/themes/default/css/author.scss +++ b/frontend/views/themes/default/css/author.scss @@ -2,7 +2,14 @@ display: flex; flex-direction: row; min-height: 50px; - margin-top: 4rem; + background-color: white; + box-shadow: #0000001c 0 0px 5px; + margin-top: 2rem; + padding: 1rem; + box-sizing: border-box; + width: 1080px; + max-width: 1080px; + border-radius: 5px; .biography { width: calc(100% * (2 / 3)); @@ -11,15 +18,24 @@ box-shadow: #0000001c 0 0px 5px; padding: 1rem; box-sizing: border-box; + + .image-container { + max-width: 100%; + + img { + max-width: 100%; + } + } } .about { width: calc(100% * (1 / 3)); background-color: white; min-height: 50px; - margin: 0 1rem; + margin: 0 0 0 1rem; padding: 1rem; box-sizing: border-box; box-shadow: #0000001c 0 0px 5px; + height: fit-content; .profile-picture { margin: auto auto 1.5rem auto; @@ -57,3 +73,9 @@ } } } + +.page .nobackground { + background-color: transparent; + box-shadow: none; + padding: 0; +} diff --git a/frontend/views/themes/default/css/generic.css b/frontend/views/themes/default/css/generic.css index c478a5a..f62a0bc 100644 --- a/frontend/views/themes/default/css/generic.css +++ b/frontend/views/themes/default/css/generic.css @@ -74,6 +74,9 @@ body { min-width: 130px; border: transparent; transition: filter ease-in-out 0.1s; + padding: 0.3rem; + box-sizing: border-box; + text-decoration: none; } .button:hover { @@ -148,6 +151,58 @@ body { margin-bottom: 1rem; } +.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; + min-width: 100%; + max-width: 100%; + box-sizing: border-box; +} + @media screen and (max-width: 1280px) { .page-center { width: 95%; diff --git a/frontend/views/themes/default/css/generic.scss b/frontend/views/themes/default/css/generic.scss index 2430c7a..e7949a7 100644 --- a/frontend/views/themes/default/css/generic.scss +++ b/frontend/views/themes/default/css/generic.scss @@ -81,6 +81,9 @@ body { min-width: 130px; border: transparent; transition: filter ease-in-out 0.1s; + padding: 0.3rem; + box-sizing: border-box; + text-decoration: none; } .button:hover { cursor: pointer; @@ -153,6 +156,68 @@ body { margin-bottom: 1rem; } +.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; + min-width: 100%; + max-width: 100%; + box-sizing: border-box; + } +} + @media screen and (max-width: 1280px) { .page-center { width: 95%; diff --git a/frontend/views/themes/default/css/newPost.css b/frontend/views/themes/default/css/newPost.css index 23c673f..40e8cf4 100644 --- a/frontend/views/themes/default/css/newPost.css +++ b/frontend/views/themes/default/css/newPost.css @@ -47,53 +47,4 @@ padding: 0.5rem; box-sizing: border-box; margin: auto; -} - -.rich-text-editor .controls { - width: 100%; - min-height: 10px; - background-color: #dbd8d8; - display: flex; - flex-direction: row; -} -.rich-text-editor .controls a { - box-sizing: border-box; - margin: 0.1rem; - cursor: pointer; - background-color: #dbd8d8; - display: flex; - flex-direction: row; - width: 2rem; - height: 2rem; -} -.rich-text-editor .controls a span { - margin: auto; -} -.rich-text-editor .controls a:hover, -.rich-text-editor .controls a:focus { - filter: brightness(80%); -} -.rich-text-editor .controls .left { - margin: 0 auto 0 0; - height: 100%; - display: flex; - flex-direction: row; -} -.rich-text-editor .controls .right { - margin: 0 0 0 auto; - display: flex; - flex-direction: row; -} -.rich-text-editor .controls .right a { - padding: 2px; - box-sizing: border-box; - display: flex; - flex-direction: row; -} -.rich-text-editor .controls .right a img { - height: 20px; - margin: auto; -} -.rich-text-editor textarea { - border-radius: 0 0 5px 5px; } \ No newline at end of file diff --git a/frontend/views/themes/default/css/newPost.scss b/frontend/views/themes/default/css/newPost.scss index 683a54c..542b2f9 100644 --- a/frontend/views/themes/default/css/newPost.scss +++ b/frontend/views/themes/default/css/newPost.scss @@ -58,62 +58,3 @@ } } } - -.rich-text-editor { - .controls { - width: 100%; - min-height: 10px; - background-color: #dbd8d8; - display: flex; - flex-direction: row; - - a { - box-sizing: border-box; - margin: 0.1rem; - cursor: pointer; - background-color: #dbd8d8; - display: flex; - flex-direction: row; - width: 2rem; - height: 2rem; - - span { - margin: auto; - } - } - - a:hover, - a:focus { - filter: brightness(80%); - } - - .left { - margin: 0 auto 0 0; - height: 100%; - display: flex; - flex-direction: row; - } - - .right { - margin: 0 0 0 auto; - display: flex; - flex-direction: row; - - a { - padding: 2px; - box-sizing: border-box; - display: flex; - flex-direction: row; - - img { - height: 20px; - margin: auto; - } - } - } - } - - textarea { - border-radius: 0 0 5px 5px; - } -} diff --git a/frontend/views/themes/default/css/settings.css b/frontend/views/themes/default/css/settings.css index 0bd99b9..315aecc 100644 --- a/frontend/views/themes/default/css/settings.css +++ b/frontend/views/themes/default/css/settings.css @@ -42,6 +42,10 @@ text-align: right; font-size: 1rem; } +.page .page-center .setting-list .setting.fit-column { + flex-direction: column; + width: 100%; +} .page .page-center .setting-list .setting:nth-child(even) { background-color: rgb(250, 250, 250); } diff --git a/frontend/views/themes/default/css/settings.scss b/frontend/views/themes/default/css/settings.scss index 0f09d3c..0b90ee5 100644 --- a/frontend/views/themes/default/css/settings.scss +++ b/frontend/views/themes/default/css/settings.scss @@ -47,6 +47,11 @@ } } + .setting.fit-column { + flex-direction: column; + width: 100%; + } + .setting:nth-child(even) { background-color: rgb(250, 250, 250); } diff --git a/frontend/views/themes/default/ejs/author.ejs b/frontend/views/themes/default/ejs/author.ejs index 4ca46a3..19a0cac 100644 --- a/frontend/views/themes/default/ejs/author.ejs +++ b/frontend/views/themes/default/ejs/author.ejs @@ -10,19 +10,28 @@ <%- include("partials/header.ejs") %>
+ <%if(logged_in_user) {%>
+
+ +
+
+ <%}%> + +
<%= post.title %>
<%- post.content %>
-
-
DISPLAYNAME
-
Registered REGISTRATIONDATE
-
NUMPOSTS Posts
+
+
<%= post.owner.display_name || post.owner.username %>
+ +
Registered <%= post.created_date.toLocaleString('en-US', { dateStyle:'medium' }) || "Null" %>
+
<%= post.post_count %> Posts
- Matrix +
diff --git a/frontend/views/themes/default/ejs/authorEdit.ejs b/frontend/views/themes/default/ejs/authorEdit.ejs new file mode 100644 index 0000000..6482852 --- /dev/null +++ b/frontend/views/themes/default/ejs/authorEdit.ejs @@ -0,0 +1,43 @@ + + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> +
+
+
User Settings
+
+
+
Display Name
+
+ +
+
+
+
Change Password
+ +
+
+
Change Profile Picture
+
+ +
+
+
+
+
+
Biography
+ <%- include("partials/richTextEditor.ejs", {text_selector: 'post-content', prefill: profile.raw_content}) %> + +
+
+ <%- include("partials/footer.ejs") %> + + + diff --git a/frontend/views/themes/default/ejs/partials/post.ejs b/frontend/views/themes/default/ejs/partials/post.ejs index 10f4695..b6b8390 100644 --- a/frontend/views/themes/default/ejs/partials/post.ejs +++ b/frontend/views/themes/default/ejs/partials/post.ejs @@ -1,6 +1,6 @@
<%= post.title ? post.title : "Untitled Post" %> - +
<%= post.description %>
diff --git a/frontend/views/themes/default/ejs/partials/richTextEditor.ejs b/frontend/views/themes/default/ejs/partials/richTextEditor.ejs new file mode 100644 index 0000000..c456836 --- /dev/null +++ b/frontend/views/themes/default/ejs/partials/richTextEditor.ejs @@ -0,0 +1,31 @@ +
+
+ +
+ + +
+
+ +
+ diff --git a/frontend/views/themes/default/ejs/post.ejs b/frontend/views/themes/default/ejs/post.ejs index 828898d..1f898a6 100644 --- a/frontend/views/themes/default/ejs/post.ejs +++ b/frontend/views/themes/default/ejs/post.ejs @@ -20,7 +20,7 @@ <%}%>
-
<%= blog_post.title%>
+
<%= blog_post.title %>
<%- blog_post.content %>
diff --git a/frontend/views/themes/default/ejs/postNew.ejs b/frontend/views/themes/default/ejs/postNew.ejs index 491d276..aebec79 100644 --- a/frontend/views/themes/default/ejs/postNew.ejs +++ b/frontend/views/themes/default/ejs/postNew.ejs @@ -27,36 +27,7 @@
Content
-
-
- -
- - -
-
- -
+ <%- include("partials/richTextEditor.ejs", {text_selector: 'post-content', prefill: existing_blog.raw_content}) %>
Post Date
@@ -67,8 +38,9 @@
- - + + +
diff --git a/frontend/views/themes/default/js/editAuthor.js b/frontend/views/themes/default/js/editAuthor.js new file mode 100644 index 0000000..6856e07 --- /dev/null +++ b/frontend/views/themes/default/js/editAuthor.js @@ -0,0 +1,12 @@ +async function changeValue(setting_name, element) { + const form = { + setting_name: setting_name, + value: element.value, + id: window.location.href.split("/")[4], + }; + const response = await request(`/api/web/user`, "PATCH", form); + + // TODO: On failure, notify the user + if (response.body.success) { + } +} diff --git a/frontend/views/themes/default/js/newPost.js b/frontend/views/themes/default/js/newPost.js index 743b556..b7e9bd3 100644 --- a/frontend/views/themes/default/js/newPost.js +++ b/frontend/views/themes/default/js/newPost.js @@ -1,86 +1,12 @@ 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() { +async function publish(visibility) { let form_data = { title: qs("#post-title").value, description: qs("#post-description").value, tags: [], - images: images, + media: media, + visibility: visibility, content: qs("#post-content").value, date: qs("#date").value, time: qs("#time").value, @@ -93,19 +19,9 @@ async function publish() { let tags_array = qs("#post-tags").value.split(","); tags_array.forEach((tag) => form_data.tags.push(tag.trim())); } - const post_response = await request("/api/web/post", "PATCH", form_data); if (post_response.body.success) { window.location.href = `/post/${post_response.body.post_id}`; } } - -// Auto resize on page load -post_content_area.style.height = post_content_area.scrollHeight + "px"; -post_content_area.style.minHeight = post_content_area.scrollHeight + "px"; -// Auto expand blog area -post_content_area.addEventListener("input", (e) => { - post_content_area.style.height = post_content_area.scrollHeight + "px"; - post_content_area.style.minHeight = e.target.scrollHeight + "px"; -}); diff --git a/frontend/views/themes/default/js/richTextEditor.js b/frontend/views/themes/default/js/richTextEditor.js new file mode 100644 index 0000000..d890d68 --- /dev/null +++ b/frontend/views/themes/default/js/richTextEditor.js @@ -0,0 +1,98 @@ +const rich_text_editors = qsa(".rich-text-editor"); +let media = []; + +function textareaAction(textarea, insert, cursor_position, dual_side = false) { + // textarea = textarea.querySelector("textarea"); + const selectionStart = textarea.selectionStart; + const selectionEnd = textarea.selectionEnd; + + const textBefore = textarea.value.substring(0, selectionStart); + const textAfter = textarea.value.substring(selectionEnd); + const selectedText = textarea.value.substring(selectionStart, selectionEnd); + + let updatedText; + + if (dual_side) updatedText = `${textBefore}${insert}${selectedText}${insert}${textAfter}`; + else updatedText = `${textBefore}${insert}${selectedText}${textAfter}`; + + textarea.value = updatedText; + + // Set the cursor position after the custom string + textarea.focus(); + const newPosition = selectionStart + (cursor_position || insert.length); + textarea.setSelectionRange(newPosition, newPosition); +} + +// Go though rich editors and apply image uploading script +rich_text_editors.forEach((editor) => { + editor.querySelector("#insert-sidebyside").addEventListener("click", () => textareaAction(editor, "{sidebyside}{/sidebyside}", 12)); + editor.querySelector("#insert-video").addEventListener("click", () => textareaAction(editor, "{video:}", 7)); + editor.querySelector("#insert-h1").addEventListener("click", () => textareaAction(editor, "# ")); + editor.querySelector("#insert-h2").addEventListener("click", () => textareaAction(editor, "## ")); + editor.querySelector("#insert-h3").addEventListener("click", () => textareaAction(editor, "### ")); + editor.querySelector("#insert-h4").addEventListener("click", () => textareaAction(editor, "#### ")); + editor.querySelector("#insert-underline").addEventListener("click", () => textareaAction(editor, "_", undefined, true)); + editor.querySelector("#insert-italics").addEventListener("click", () => textareaAction(editor, "*", undefined, true)); + editor.querySelector("#insert-bold").addEventListener("click", () => textareaAction(editor, "__", undefined, true)); + editor.querySelector("#insert-strike").addEventListener("click", () => textareaAction(editor, "~~", undefined, true)); + editor.querySelector("#insert-sup").addEventListener("click", () => textareaAction(editor, "^", undefined, true)); + + editor.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: window.location.href.split("/")[4], + }; + + const image_uploading_request = await request("/api/web/image", "POST", form_data); + + if (image_uploading_request.status == 200) { + textareaAction(editor, `{image:${image_uploading_request.body}}`); + media.push(image_uploading_request.body); + } + } + }); + + let textarea = editor.querySelector("textarea"); + textarea.addEventListener("input", (e) => { + textarea.style.height = textarea.scrollHeight + "px"; + textarea.style.minHeight = e.target.scrollHeight + "px"; + }); + + // Auto resize on page load + textarea.style.height = textarea.scrollHeight + "px"; + textarea.style.minHeight = textarea.scrollHeight + "px"; +}); + +// 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 updateBiography() { + let form_data = { + media: media, + content: qs("#post-content").value, + id: window.location.href.split("/")[4], + }; + + const post_response = await request("/api/web/biography", "PATCH", form_data); + + if (post_response.body.success) { + window.location.href = `/post/${post_response.body.post_id}`; + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 55420fd..1d8d508 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,32 +11,31 @@ datasource db { } model User { - id String @id @unique @default(uuid()) - username String @unique - password String + id String @id @unique @default(uuid()) + username String @unique + password String + display_name String? - role Role @default(USER) - group String? + role Role @default(USER) - blog_posts BlogPost[] + blog_posts Post[] profile_page ProfilePage? @@index([username, role]) } -model BlogPost { +model Post { id String @id @unique @default(uuid()) title String? description String? content String? - thumbnail String? - images String[] + media String[] visibility PostStatus @default(UNLISTED) owner User? @relation(fields: [ownerid], references: [id], onDelete: Cascade) ownerid String? // Tags - tags String[] + tags String[] // Dates publish_date DateTime? @@ -44,12 +43,13 @@ model BlogPost { } model ProfilePage { - id String @id @unique @default(uuid()) - content String? - images String[] - visibility PostStatus @default(UNLISTED) - owner User @relation(fields: [ownerid], references: [id], onDelete: Cascade) - ownerid String @unique + id String @id @unique @default(uuid()) + content String? + media String[] + visibility PostStatus @default(UNLISTED) + owner User @relation(fields: [ownerid], references: [id], onDelete: Cascade) + ownerid String @unique + created_date DateTime @default(now()) } model Setting { @@ -69,6 +69,7 @@ enum Role { } enum PostStatus { + PRIVATE UNLISTED PUBLISHED } diff --git a/yab.js b/yab.js index f75ae9f..47f93df 100644 --- a/yab.js +++ b/yab.js @@ -36,6 +36,8 @@ 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.patch("/api/web/biography", checkAuthenticated, internal.patchBiography); +app.patch("/api/web/user", checkAuthenticated, internal.patchUser); // app.delete("/logout", page_scripts.logout); @@ -44,6 +46,7 @@ app.get("/", page_scripts.index); app.get("/login", page_scripts.login); app.get("/register", checkNotAuthenticated, page_scripts.register); app.get("/author/:author_id", page_scripts.author); +app.get("/author/:author_id/edit", checkAuthenticated, page_scripts.authorEdit); app.get("/admin", checkAuthenticated, page_scripts.admin); app.get("/posts", page_scripts.blogList); app.get("/post/new", checkAuthenticated, page_scripts.blogNew);