From fc83b5bbe9b4c18f0e51b6f64a60e7050c16b696 Mon Sep 17 00:00:00 2001 From: Armored-Dragon Date: Tue, 30 Apr 2024 10:26:35 -0500 Subject: [PATCH] Generic Theme (#1) * Theme work Signed-off-by: Armored Dragon * User registration. Cleanup CSS. Signed-off-by: Armored Dragon * Post Creation and Manipulation Uploading images now easier. Just drag and drop onto the text area. Signed-off-by: Armored Dragon * Author Page. Edit author page. Author display name. Generic media uploads. Core refactoring. Signed-off-by: Armored Dragon * Texteditor bugfix. PGAdmin docker container for management of database. Signed-off-by: Armored Dragon * Tags. Search by tags. Return tags used by posts. Signed-off-by: Armored Dragon * New post button. Fix index "page" param not being honored. Signed-off-by: Armored Dragon * Post drafts Users can now only have one "unpublished" draft. Improved password handling. Minor cleanup. Admin panel navigation link. Signed-off-by: Armored Dragon * Post visibility flairs Signed-off-by: Armored Dragon * Publish date autofill to now. Fix deleteBlog. Signed-off-by: Armored Dragon * Removed unused function Signed-off-by: Armored Dragon * Media upload pruning. Uploaded media is now pruned automatically every time a post is updated. Minor cleanup. Groundwork for media types other than images. Signed-off-by: Armored Dragon * Updated name. Use the manifest data. Signed-off-by: Armored Dragon --------- Signed-off-by: Armored Dragon --- backend/core/core.js | 642 ++++++++++-------- backend/core/external_api.js | 3 +- backend/core/internal_api.js | 45 +- backend/form_validation.js | 2 +- backend/page_scripts.js | 101 ++- docker-compose.yml | 12 + frontend/public/css/admin.css | 252 ------- frontend/public/css/admin.scss | 101 --- frontend/public/css/blog-list.css | 128 ---- frontend/public/css/blog-list.scss | 142 ---- frontend/public/css/blogNew.css | 192 ------ frontend/public/css/blogNew.scss | 211 ------ frontend/public/css/blogSingle.css | 68 -- frontend/public/css/blogSingle.scss | 86 --- frontend/public/css/signin.css | 217 ------ frontend/public/css/signin.scss | 57 -- frontend/public/css/theme.css | 168 ----- frontend/public/css/theme.scss | 186 ----- frontend/public/js/admin.js | 72 -- frontend/public/js/blogSingle.js | 8 - frontend/public/js/login.js | 17 - frontend/public/js/newBlog.js | 257 ------- frontend/views/admin.ejs | 56 -- frontend/views/author.ejs | 32 - frontend/views/blogList.ejs | 33 - frontend/views/blogNew.ejs | 105 --- frontend/views/blogSingle.ejs | 35 - frontend/views/index.ejs | 23 - frontend/views/login.ejs | 36 - .../views/partials/admin-setting-number.ejs | 15 - .../views/partials/admin-setting-text.ejs | 15 - .../views/partials/admin-setting-toggle.ejs | 13 - frontend/views/partials/blog-admin.ejs | 5 - frontend/views/partials/blog-entry.ejs | 16 - frontend/views/partials/footer.ejs | 0 frontend/views/partials/header.ejs | 21 - frontend/views/partials/pagination.ejs | 29 - frontend/views/register.ejs | 36 - frontend/views/themes/default/css/author.css | 73 ++ frontend/views/themes/default/css/author.scss | 81 +++ frontend/views/themes/default/css/generic.css | 252 +++++++ .../views/themes/default/css/generic.scss | 274 ++++++++ frontend/views/themes/default/css/index.css | 195 ++++++ frontend/views/themes/default/css/index.scss | 215 ++++++ frontend/views/themes/default/css/login.css | 38 ++ frontend/views/themes/default/css/login.scss | 44 ++ frontend/views/themes/default/css/newPost.css | 50 ++ .../views/themes/default/css/newPost.scss | 60 ++ frontend/views/themes/default/css/post.css | 27 + frontend/views/themes/default/css/post.scss | 33 + .../views/themes/default/css/settings.css | 106 +++ .../views/themes/default/css/settings.scss | 110 +++ .../themes/default/ejs/admin-settings.ejs | 72 ++ frontend/views/themes/default/ejs/author.ejs | 42 ++ .../views/themes/default/ejs/authorEdit.ejs | 43 ++ frontend/views/themes/default/ejs/index.ejs | 44 ++ frontend/views/themes/default/ejs/login.ejs | 34 + .../themes/default/ejs/partials/footer.ejs | 11 + .../themes/default/ejs/partials/header.ejs | 30 + .../default/ejs/partials/pagination.ejs | 23 + .../themes/default/ejs/partials/post.ejs | 20 + .../default/ejs/partials/richTextEditor.ejs | 31 + frontend/views/themes/default/ejs/post.ejs | 31 + frontend/views/themes/default/ejs/postNew.ejs | 51 ++ .../views/themes/default/ejs/postSearch.ejs | 37 + .../views/themes/default/ejs/register.ejs | 34 + .../views/themes/default/img/calendar.svg | 1 + .../views/themes/default/img/hourglass.svg | 1 + frontend/views/themes/default/img/json.svg | 1 + frontend/views/themes/default/img/news.svg | 1 + frontend/views/themes/default/img/rss.svg | 1 + frontend/views/themes/default/img/tag.svg | 1 + .../default}/img/textarea/sidebyside.svg | 8 +- .../themes/default}/img/textarea/video.svg | 10 +- frontend/views/themes/default/js/admin.js | 24 + .../views/themes/default/js/editAuthor.js | 12 + .../themes/default}/js/generic.js | 0 .../themes/default/js/login.js} | 18 + frontend/views/themes/default/js/newPost.js | 27 + frontend/views/themes/default/js/post.js | 6 + .../themes/default/js/postSearch.js} | 2 - .../views/themes/default/js/richTextEditor.js | 101 +++ frontend/views/themes/default/manifest.json | 14 + prisma/schema.prisma | 50 +- yab.js | 24 +- 85 files changed, 2799 insertions(+), 3001 deletions(-) delete mode 100644 frontend/public/css/admin.css delete mode 100644 frontend/public/css/admin.scss delete mode 100644 frontend/public/css/blog-list.css delete mode 100644 frontend/public/css/blog-list.scss delete mode 100644 frontend/public/css/blogNew.css delete mode 100644 frontend/public/css/blogNew.scss delete mode 100644 frontend/public/css/blogSingle.css delete mode 100644 frontend/public/css/blogSingle.scss delete mode 100644 frontend/public/css/signin.css delete mode 100644 frontend/public/css/signin.scss delete mode 100644 frontend/public/css/theme.css delete mode 100644 frontend/public/css/theme.scss delete mode 100644 frontend/public/js/admin.js delete mode 100644 frontend/public/js/blogSingle.js delete mode 100644 frontend/public/js/login.js delete mode 100644 frontend/public/js/newBlog.js delete mode 100644 frontend/views/admin.ejs delete mode 100644 frontend/views/author.ejs delete mode 100644 frontend/views/blogList.ejs delete mode 100644 frontend/views/blogNew.ejs delete mode 100644 frontend/views/blogSingle.ejs delete mode 100644 frontend/views/index.ejs delete mode 100644 frontend/views/login.ejs delete mode 100644 frontend/views/partials/admin-setting-number.ejs delete mode 100644 frontend/views/partials/admin-setting-text.ejs delete mode 100644 frontend/views/partials/admin-setting-toggle.ejs delete mode 100644 frontend/views/partials/blog-admin.ejs delete mode 100644 frontend/views/partials/blog-entry.ejs delete mode 100644 frontend/views/partials/footer.ejs delete mode 100644 frontend/views/partials/header.ejs delete mode 100644 frontend/views/partials/pagination.ejs delete mode 100644 frontend/views/register.ejs create mode 100644 frontend/views/themes/default/css/author.css create mode 100644 frontend/views/themes/default/css/author.scss create mode 100644 frontend/views/themes/default/css/generic.css create mode 100644 frontend/views/themes/default/css/generic.scss create mode 100644 frontend/views/themes/default/css/index.css create mode 100644 frontend/views/themes/default/css/index.scss create mode 100644 frontend/views/themes/default/css/login.css create mode 100644 frontend/views/themes/default/css/login.scss create mode 100644 frontend/views/themes/default/css/newPost.css create mode 100644 frontend/views/themes/default/css/newPost.scss create mode 100644 frontend/views/themes/default/css/post.css create mode 100644 frontend/views/themes/default/css/post.scss create mode 100644 frontend/views/themes/default/css/settings.css create mode 100644 frontend/views/themes/default/css/settings.scss create mode 100644 frontend/views/themes/default/ejs/admin-settings.ejs create mode 100644 frontend/views/themes/default/ejs/author.ejs create mode 100644 frontend/views/themes/default/ejs/authorEdit.ejs create mode 100644 frontend/views/themes/default/ejs/index.ejs create mode 100644 frontend/views/themes/default/ejs/login.ejs create mode 100644 frontend/views/themes/default/ejs/partials/footer.ejs create mode 100644 frontend/views/themes/default/ejs/partials/header.ejs create mode 100644 frontend/views/themes/default/ejs/partials/pagination.ejs create mode 100644 frontend/views/themes/default/ejs/partials/post.ejs create mode 100644 frontend/views/themes/default/ejs/partials/richTextEditor.ejs create mode 100644 frontend/views/themes/default/ejs/post.ejs create mode 100644 frontend/views/themes/default/ejs/postNew.ejs create mode 100644 frontend/views/themes/default/ejs/postSearch.ejs create mode 100644 frontend/views/themes/default/ejs/register.ejs create mode 100644 frontend/views/themes/default/img/calendar.svg create mode 100644 frontend/views/themes/default/img/hourglass.svg create mode 100644 frontend/views/themes/default/img/json.svg create mode 100644 frontend/views/themes/default/img/news.svg create mode 100644 frontend/views/themes/default/img/rss.svg create mode 100644 frontend/views/themes/default/img/tag.svg rename frontend/{public => views/themes/default}/img/textarea/sidebyside.svg (87%) rename frontend/{public => views/themes/default}/img/textarea/video.svg (88%) create mode 100644 frontend/views/themes/default/js/admin.js create mode 100644 frontend/views/themes/default/js/editAuthor.js rename frontend/{public => views/themes/default}/js/generic.js (100%) rename frontend/{public/js/register.js => views/themes/default/js/login.js} (50%) create mode 100644 frontend/views/themes/default/js/newPost.js create mode 100644 frontend/views/themes/default/js/post.js rename frontend/{public/js/postList.js => views/themes/default/js/postSearch.js} (75%) create mode 100644 frontend/views/themes/default/js/richTextEditor.js create mode 100644 frontend/views/themes/default/manifest.json diff --git a/backend/core/core.js b/backend/core/core.js index 459272e..85d8a58 100644 --- a/backend/core/core.js +++ b/backend/core/core.js @@ -4,6 +4,7 @@ const sharp = require("sharp"); const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsCommand, DeleteObjectsCommand } = require("@aws-sdk/client-s3"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); let s3; +const crypto = require("crypto"); const md = require("markdown-it")() .use(require("markdown-it-underline")) .use(require("markdown-it-footnote")) @@ -21,23 +22,19 @@ let settings = { HIDE_LOGIN: false, BLOG_UPLOADING: false, - CD_RSS: false, - CD_AP: false, + CD_RSS: true, + CD_JSON: true, WEBSITE_NAME: "", PLAUSIBLE_URL: "", USER_MINIMUM_PASSWORD_LENGTH: 7, - BLOG_MINIMUM_TITLE_LENGTH: 7, - BLOG_MINIMUM_DESCRIPTION_LENGTH: 7, - BLOG_MINIMUM_CONTENT_LENGTH: 7, + theme: "default", }; let use_s3_storage = false; -let groups = []; _initS3Storage(); _getSettings(); -_getGroups(); // Checks to see if S3 storage is set function _initS3Storage() { @@ -60,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) { @@ -84,27 +78,86 @@ 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, include_password = false }) { + 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"); + + // Delete the password from responses + if (!include_password) delete user.password; + + 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 }) { + // TODO: Validate request (Does user have perms?) + // TODO: Does server allow new posts? - // Render the post - const rendered_post = await _renderPost(post, true); + // Find if user already has a draft + let existing_post = await prisma.post.findFirst({ where: { owner: { id: requester_id }, visibility: "DRAFT" } }); + if (existing_post) return existing_post.id; - // Return the post with valid image urls - return { data: rendered_post, success: true }; + const post = await prisma.post.create({ data: { owner: { connect: { id: requester_id } } } }); + + 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, tags: true } }); + if (!post) return _r(false, "Post does not exist"); + post = _stripPrivatePost(post); + + // Tags + let post_tags = []; + post.raw_tags = []; + post.tags.forEach((tag) => { + post_tags.push(tag.name); + post.raw_tags.push(); + }); + post.tags = post_tags; + + // 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 @@ -123,7 +176,7 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag // User owns the post { - ownerid: owner_id, + ownerid: requester_id, }, ], @@ -133,193 +186,210 @@ 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_tags) where_object["AND"][0]["OR"].push({ tags: { some: { name: 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, - include: { owner: true }, + include: { owner: true, tags: true }, 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); + + let post_tags = []; + post.tags.forEach((tag) => post_tags.push(tag.name)); + post.tags = post_tags; } + // 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 }; -} -async function getAuthorPage({ author_id }) { - // Get the post by the id - let post = await prisma.profilePage.findUnique({ where: { ownerid: author_id }, include: { owner: true } }); - if (!post) return { success: false, message: "Post does not exist" }; + return { data: post_list, pagination: _getNavigationList(page, Math.ceil(post_count / limit)), success: true }; - // Render the post - const rendered_post = await _renderPost(post, true); + function _getNavigationList(current_page, max_page) { + current_page = Number(current_page); + max_page = Number(max_page); - // 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 } }); - - 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, - }; - - // Save to database - const database_blog = await prisma.blogPost.create({ data: { ...blog_post_formatted, owner: { connect: { id: owner_id } } } }); - - // Init image vars - let uploaded_images = []; - let uploaded_thumbnail = "DEFAULT"; - - // For Each image, upload to S3 - if (blog_post.images) { - for (let i = 0; blog_post.images.length > i; i++) { - const image = blog_post.images[i]; - const image_data = Buffer.from(image.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(database_blog.id, "blog", false, image_data, image.id); - if (name) uploaded_images.push(name); - } + 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); } - - // Upload thumbnail to S3 - if (blog_post.thumbnail) { - const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(database_blog.id, "blog", true, image_data, blog_post.thumbnail.id); - if (name) uploaded_thumbnail = name; - } - - // Update the blog post to include references to our images - await prisma.blogPost.update({ where: { id: database_blog.id }, data: { images: uploaded_images, thumbnail: uploaded_thumbnail } }); - return { success: true, blog_id: database_blog.id }; } -async function deleteBlog(blog_id, requester_id) { - const user = await getUser({ id: requester_id }); - const post = await getBlog({ id: blog_id }); - - if (!post.success) return { success: false, message: post.message || "Post does not exist" }; - - 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 } }); - _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 }); +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; - delete blog_post.id; + 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; - 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" }; + // Check to see if the requester can update the post + // TODO: Permissions + let can_update = post.owner.id === user.id || user.role === "ADMIN"; // 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(":"); + 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); } - 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, - }; + // Handle tags ---- + let database_tag_list = []; + const existing_tags = post.tags?.map((tag) => ({ name: tag })) || []; - await prisma.blogPost.update({ where: { id: post.data.id }, data: blog_post_formatted }); + // Add new tags + for (let tag_index = 0; post_content.tags.length > tag_index; tag_index++) { + let tag = post_content.tags[tag_index]; - let uploaded_images = []; - let uploaded_thumbnail = "DEFAULT"; + // Check to see if tag exists, create if necessary, + let database_tag = await prisma.tag.upsert({ where: { name: tag }, update: {}, create: { name: tag } }); - // For Each image, upload to S3 - if (blog_post.images) { - for (let i = 0; blog_post.images.length > i; i++) { - const image = blog_post.images[i]; - const image_data = Buffer.from(image.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(post.id, "blog", false, image_data, image.id); - if (name) uploaded_images.push(name); - } + database_tag_list.push(database_tag); } - let data_to_update = { - images: [...post.data.raw_images, ...uploaded_images], + // 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: { disconnect: [...existing_tags], connect: [...database_tag_list] }, + media: [...post.raw_media, ...post_content.media], }; - if (blog_post.thumbnail) { - const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64"); - const name = await _uploadImage(post.data.id, "blog", true, image_data, blog_post.thumbnail.id); - if (name) uploaded_thumbnail = name; + // Save the updated post to the database + await prisma.post.update({ where: { id: post.id }, data: post_formatted }); - data_to_update.thumbnail = uploaded_thumbnail; - } + // Prune the post to save on storage + await pruneMedia({ parent_id: post_id, parent_type: "posts" }); - await prisma.blogPost.update({ where: { id: post.data.id }, data: data_to_update }); + return _r(true); +} +async function deletePost({ requester_id, post_id }) { + let user = await getUser({ user_id: requester_id }); + let post = await getPost({ post_id: post_id }); + + if (!user.success) return { success: false, message: user.message || "User does not exist" }; + user = user.data; + + if (!post.success) return { success: false, message: post.message || "Post does not exist" }; + post = post.data; + + let can_delete = post.owner.id === user.id || user.role === "ADMIN"; + + if (!can_delete) return { success: false, message: "Action not permitted" }; + + await prisma.post.delete({ where: { id: post.id } }); + _deleteS3Directory(post.id, "post"); 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 }); +// 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 post exists - if (!post) return { success: false, message: "Post does not exist" }; + // Check if it is private + // TODO - // Check for permissions - if (post.owner.id !== user.data.id || user.data.role !== "ADMIN") return { success: false, message: "User is not permitted" }; + // 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 = {}; - let image_index = post.raw_images.indexOf(image.id); + rendering_formatted_post = post; + rendering_formatted_post.id = author_id; - post.raw_images.splice(image_index, 1); + // Render + post = _stripPrivatePost(post); + post = await _renderPost(rendering_formatted_post); - await prisma.blogPost.update({ where: { id: post.id }, data: { images: post.raw_images } }); + 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); +} +async function uploadMedia({ parent_id, parent_type, file_buffer, content_type }) { + if (!use_s3_storage) return null; + const content_name = crypto.randomUUID(); + let maximum_image_resolution = { width: 1920, height: 1080 }; + + // 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(); + + let extension; + let s3_content_type; + + if (content_type.includes("image/")) { + extension = ".webp"; + s3_content_type = "image/webp"; + } + + const params = { + Bucket: process.env.S3_BUCKET_NAME, + Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${content_name}${extension}`, + Body: compressed_image, + ContentType: s3_content_type, + }; + + const command = new PutObjectCommand(params); + await s3.send(command); + + return content_name + extension; +} +async function getMedia({ parent_id, parent_type, file_name }) { + if (!use_s3_storage) return null; + const params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${file_name}` }; + return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 }); +} + +async function deleteMedia({ parent_id, parent_type, file_name }) { const request_params = { Bucket: process.env.S3_BUCKET_NAME, - Key: `${process.env.ENVIRONMENT}/${image.parent_type}/${image.parent}/${image.id}.webp`, + Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${file_name}`, }; const command = new DeleteObjectCommand(request_params); @@ -327,40 +397,58 @@ async function deleteImage(image, requester_id) { return { success: true }; } -async function _uploadImage(parent_id, parent_type, is_thumbnail, buffer, name) { - if (!use_s3_storage) return null; - let size = { width: 1920, height: 1080 }; - if (is_thumbnail) size = { width: 300, height: 300 }; - const compressed_image = await sharp(buffer, { animated: true }) - .resize({ ...size, withoutEnlargement: true, fit: "inside" }) - .webp({ quality: 90, animated: true }) - .toBuffer(); +// This cleans up all unused and unreferenced media files. +// NOTE: Only made for posts, as that is all that there is right now +async function pruneMedia({ parent_id, parent_type }) { + let post = await getPost({ post_id: parent_id }); - const params = { - Bucket: process.env.S3_BUCKET_NAME, - Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${name}.webp`, - Body: compressed_image, - ContentType: "image/webp", - }; + if (!post.success) return { success: false, message: post.message || "Post does not exist" }; + post = post.data; - const command = new PutObjectCommand(params); - await s3.send(command); + // const total_number_of_media = post.raw_media.length; - return name; + for (let media_index = 0; post.raw_media.length > media_index; media_index++) { + if (!post.raw_content.includes(post.raw_media[media_index])) { + // Delete the media off of the S3 server + let delete_request = await deleteMedia({ parent_id: parent_id, parent_type: parent_type, file_name: post.raw_media[media_index] }); + if (!delete_request.success) continue; + + // Remove from the list in the database + await post.raw_media.splice(media_index, 1); + // Save in the database + await prisma.post.update({ where: { id: parent_id }, data: { media: post.raw_media } }); + + // Delete was successful, move the index back to account for new array length + media_index--; + } + } } -async function _getImage(parent_id, parent_type, 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` }; - return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 }); +async function getTags({ order = "count" } = {}) { + if (order == "count") { + return await prisma.tag.findMany({ + include: { _count: { select: { posts: true } } }, + where: { + posts: { + some: {}, + }, + }, + take: 15, + orderBy: { + posts: { + _count: "desc", + }, + }, + }); + } } + +// TODO: +// Will be done automatically in the background +async function deleteTag({ tag_id }) {} + 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}` }; @@ -387,93 +475,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, { post_type = "blog" } = {}) { - 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; - } +async function _renderPost(post) { + post.raw_media = []; + post.raw_content = post.content; - if (blog_post.images) { - // Get the image urls for the post - for (i = 0; blog_post.images.length > i; i++) { - blog_post.images[i] = await _getImage(blog_post.id, post_type, blog_post.images[i]); + // 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, parent_type: "posts", file_name: post.media[i] }); } } - // get thumbnail URL - blog_post.thumbnail = await _getImage(blog_post.id, post_type, blog_post.thumbnail); - - 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) => { @@ -491,7 +568,7 @@ async function _getSettings() { return (settings[key] = value); }); } - +// TODO: Replace async function getSetting(key, { parse = true }) { if (!settings[key]) return null; @@ -500,19 +577,44 @@ 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" }; await prisma.setting.upsert({ where: { id: key }, update: { value: value }, create: { id: key, value: value } }); - settings[key] = JSON.parse(value); + try { + settings[key] = JSON.parse(value); + } catch { + settings[key] = value; + } return { success: true }; } catch (e) { return { success: false, message: e.message }; } } -async function _getGroups() { - const group_list = await prisma.group.findMany(); +// TODO: Replace +async function editSetting({ name, value }) { + if (!Object.keys(settings).includes(name)) return _r(false, "Setting is not valid"); + + 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); } -module.exports = { settings, registerUser, getUser, getAuthorPage, postBlog, updateBlog, getBlog, deleteBlog, deleteImage, postSetting, getSetting }; + +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, deletePost, getBiography, updateBiography, uploadMedia, getTags, postSetting, getSetting }; diff --git a/backend/core/external_api.js b/backend/core/external_api.js index 457c1ee..986c085 100644 --- a/backend/core/external_api.js +++ b/backend/core/external_api.js @@ -1,7 +1,6 @@ const feed_lib = require("feed").Feed; const core = require("./core"); -// TODO: Expose ATOM Feed items function getBaseFeed() { return new feed_lib({ title: core.settings.WEBSITE_NAME, @@ -24,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 cf1f052..8bc5354 100644 --- a/backend/core/internal_api.js +++ b/backend/core/internal_api.js @@ -18,13 +18,13 @@ 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 // Get the user by username - const existing_user = await core.getUser({ username: username }); + const existing_user = await core.getUser({ username: username, include_password: true }); if (!existing_user.success) return res.json({ success: false, message: existing_user.message }); // Check the password @@ -36,36 +36,25 @@ async function postLogin(req, res) { res.json({ success: true }); } async function postSetting(request, response) { - const user = await core.getUser({ id: request.session.user.id }); + const user = await core.getUser({ user_id: request.session.user.id }); if (!user.success) return response.json({ success: false, message: user.message }); if (user.data.role !== "ADMIN") return response.json({ success: false, message: "User is not permitted" }); response.json(await core.postSetting(request.body.setting_name, request.body.value)); } +async function postImage(request, response) { + // TODO: Permissions for uploading images + // TODO: Verification for image uploading + return response.json(await core.uploadMedia({ parent_id: request.body.post_id, parent_type: request.body.parent_type, file_buffer: request.body.buffer, content_type: request.body.content_type })); +} async function deleteImage(req, res) { // TODO: Permissions for deleting image return res.json(await core.deleteImage(req.body, req.session.user.id)); } -async function postBlog(req, res) { - // Get user - const user = await core.getUser({ id: req.session.user.id }); - if (!user.success) return user; - - // TODO: Permissions for uploading posts - // Can user upload? - // const permissions = await permissions.postBlog(user); - - // TODO: Validation for uploading posts - // Validate blog info - const valid = await validate.postBlog(req.body); - - // Upload blog post - return res.json(await core.postBlog(valid.data, req.session.user.id)); -} async function deleteBlog(req, res) { // TODO: Permissions for deleting blog - return res.json(await core.deleteBlog(req.body.id, req.session.user.id)); + return res.json(await core.deletePost({ post_id: req.body.id, requester_id: req.session.user.id })); } async function patchBlog(req, res) { // FIXME: validate does not return post id @@ -73,10 +62,20 @@ 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, deleteImage, postBlog, deleteBlog, patchBlog }; +module.exports = { postRegister, patchBiography, postLogin, postSetting, postImage, deleteImage, 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 ecd7216..96e6035 100644 --- a/backend/page_scripts.js +++ b/backend/page_scripts.js @@ -1,35 +1,73 @@ const external = require("./core/external_api"); const core = require("./core/core"); -function getDefaults(req) { - // TODO: Fix reference to website_name - return { logged_in_user: req.session.user, website_name: core.settings.WEBSITE_NAME || "Yet-Another-Blog", settings: core.settings }; +function _getThemePage(page_name) { + let manifest = require(`../frontend/views/themes/${core.settings.theme}/manifest.json`); + return `themes/${core.settings.theme}/${manifest.pages[page_name]}`; } +async function getDefaults(req) { + // TODO: Fix reference to website_name + let user; + if (req.session.user) user = await core.getUser({ user_id: req.session.user.id }); + if (user?.success) user = user.data; + return { logged_in_user: user, website_name: core.settings.WEBSITE_NAME || "Yet-Another-Blog", settings: core.settings }; +} async function index(request, response) { // Check if the master admin has been created - const is_setup_complete = core.settings["SETUP_COMPLETE"]; - if (!is_setup_complete) return response.redirect("/register"); + // const is_setup_complete = core.settings["SETUP_COMPLETE"]; + // if (!is_setup_complete) return response.redirect("/register"); - response.redirect("/blog"); + const blog_list = await core.getPost({ requester_id: request.session.user?.id }, {}, { page: request.query.page || 0 }); + const tags = await core.getTags(); + + 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"), { + ...(await getDefaults(request)), + blog_list: blog_list.data, + pagination: blog_list.pagination, + current_page: request.query.page || 0, + loaded_page: request.path, + tags: tags, + }); } -function register(request, response) { - response.render("register.ejs", getDefaults(request)); +async function register(request, response) { + response.render(_getThemePage("register"), await getDefaults(request)); } -function login(request, response) { - response.render("login.ejs", getDefaults(request)); +async function login(request, response) { + response.render(_getThemePage("login"), await 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 }); - res.render("author.ejs", { ...getDefaults(req), blog_post: profile.data }); + const profile = await core.getBiography({ author_id: user.data.id }); + // TODO: Check for success + const posts = await core.getPost({ requester_id: user.data.id }); + + res.render(_getThemePage("author"), { ...(await 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"), { ...(await 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 }); - res.render("blogList.ejs", { - ...getDefaults(req), + const blog_list = await core.getPost({ requester_id: req.session.user?.id }, { search: req.query.search, search_title: true, search_tags: true, search_content: 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"), { + ...(await getDefaults(req)), blog_list: blog_list.data, pagination: blog_list.pagination, current_page: req.query.page || 0, @@ -37,26 +75,18 @@ async function blogList(req, res) { }); } async function blogSingle(req, res) { - const blog = await core.getBlog({ id: req.params.blog_id }); - if (blog.success === false) return res.redirect("/blog"); - res.render("blogSingle.ejs", { ...getDefaults(req), blog_post: blog.data }); + const blog = await core.getPost({ post_id: req.params.blog_id }); + if (blog.success === false) return res.redirect("/"); + res.render(_getThemePage("post"), { ...(await getDefaults(req)), blog_post: blog.data }); } -function blogNew(request, response) { - // TODO: Turn date formatting into function - let existing_blog = {}; - let published_date_parts = new Date().toLocaleDateString().split("/"); - const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`; - existing_blog.publish_date = formatted_date; - - let published_time_parts = new Date().toLocaleTimeString([], { timeStyle: "short" }).slice(0, 4).split(":"); - const formatted_time = `${published_time_parts[0].padStart(2, "0")}:${published_time_parts[1].padStart(2, "0")}`; - existing_blog.publish_time = formatted_time; - - response.render("blogNew.ejs", { ...getDefaults(request), existing_blog: existing_blog }); +async function blogNew(request, response) { + const new_post = await core.newPost({ 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 }); - if (existing_blog.success) existing_blog = existing_blog.data; // FIXME: Quickfix for .success/.data issue + let existing_blog = await core.getPost({ post_id: req.params.blog_id }); + if (!existing_blog.success) return res.redirect("/"); + existing_blog = existing_blog.data; let published_time_parts = new Date(existing_blog.publish_date).toLocaleTimeString([], { timeStyle: "short" }).slice(0, 4).split(":"); const formatted_time = `${published_time_parts[0].padStart(2, "0")}:${published_time_parts[1].padStart(2, "0")}`; @@ -66,10 +96,10 @@ async function blogEdit(req, res) { const formatted_date = `${published_date_parts[2]}-${published_date_parts[0].padStart(2, "0")}-${published_date_parts[1].padStart(2, "0")}`; existing_blog.publish_date = formatted_date; - res.render("blogNew.ejs", { ...getDefaults(req), existing_blog: existing_blog }); + res.render(_getThemePage("postNew"), { ...(await getDefaults(req)), existing_blog: existing_blog }); } async function admin(request, response) { - response.render("admin.ejs", { ...getDefaults(request) }); + response.render(_getThemePage("admin-settings"), { ...(await getDefaults(request)) }); } async function atom(req, res) { res.type("application/xml"); @@ -93,4 +123,5 @@ module.exports = { admin, atom, jsonFeed, + authorEdit, }; diff --git a/docker-compose.yml b/docker-compose.yml index a5ddd6c..e17f748 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,18 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_NAME} + pgadmin: + container_name: pgadmin + image: dpage/pgadmin4 + depends_on: + - db + ports: + - "5050:80" + environment: + PGADMIN_DEFAULT_EMAIL: root@root.com + PGADMIN_DEFAULT_PASSWORD: ${POSTGRES_ADMIN_PASSWORD} + restart: unless-stopped + blog: build: . container_name: yab-app diff --git a/frontend/public/css/admin.css b/frontend/public/css/admin.css deleted file mode 100644 index 8d2be6a..0000000 --- a/frontend/public/css/admin.css +++ /dev/null @@ -1,252 +0,0 @@ -body { - margin: 0; - background-color: #111; - color: white; - font-family: Verdana, Geneva, Tahoma, sans-serif; -} - -.header { - background-color: #222; - width: 100%; - height: 50px; - display: flex; - flex-direction: row; - padding: 0 10px; - box-sizing: border-box; - margin-bottom: 1rem; -} -.header .left { - margin: auto auto auto 0; - font-size: 20px; - height: 100%; -} -.header .left a { - width: inherit; -} -.header .right { - margin: auto 0 auto auto; - height: 100%; - display: flex; - flex-direction: row; -} -.header .right a:hover, -.header .right a:focus { - background-color: #333; -} -.header a { - height: 100%; - width: 130px; - margin: auto 0; - display: flex; - text-decoration: none; - transition: background-color ease-in-out 0.1s; -} -.header a div { - margin: auto; - color: white; -} - -button { - background-color: #1c478a; - border: 0; - border-radius: 5px; - color: white; - cursor: pointer; -} - -button:hover, -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; - margin: 0 auto; -} -.page .horizontal-button-container { - display: flex; - flex-direction: row; -} -.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%; - } -} -.container { - background-color: #222; - min-height: 10px; - padding: 10px 20px; - box-sizing: border-box; -} -.container .category-navigation { - width: 100%; - height: 30px; - display: flex; - margin-bottom: 15px; -} -.container .category-navigation button { - width: 100%; - margin-right: 5px; - border-radius: 5px; - text-decoration: none; - display: flex; -} -.container .category-navigation button span { - margin: auto; - color: white; -} -.container .category-navigation button:not(.active) { - background-color: #4c515e; -} -.container .category-navigation button:hover, -.container .category-navigation button:focus { - filter: brightness(50%); -} -.container .setting-row { - display: flex; - flex-direction: row; - min-height: 30px; - padding: 5px; - box-sizing: border-box; -} -.container .setting-row .setting-title { - font-size: 18px; - margin: auto auto auto 0; -} -.container .setting-row .setting-toggleable { - min-width: 150px; - display: flex; - margin: auto 0 auto auto; -} -.container .setting-row .setting-toggleable .spinner { - margin: auto 0 auto auto; - animation: spin 1s; - animation-timing-function: linear; - animation-iteration-count: infinite; -} -.container .setting-row .setting-toggleable input { - padding: 0; - margin: auto 0 auto auto; - height: 25px; - width: 100px; - box-sizing: border-box; - text-align: center; - border-radius: 10px; -} -.container .setting-row .setting-toggleable input[type=text] { - width: 350px; -} -.container .setting-row .setting-toggleable button { - width: 125px; - margin-left: auto; - height: 30px; -} -.container .setting-row:nth-child(even) { - background-color: #1c1c1c; -} -.container .setting-row:nth-child(odd) { - background-color: #191919; -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(-360deg); - } -} \ No newline at end of file diff --git a/frontend/public/css/admin.scss b/frontend/public/css/admin.scss deleted file mode 100644 index 92eb208..0000000 --- a/frontend/public/css/admin.scss +++ /dev/null @@ -1,101 +0,0 @@ -@use "theme"; - -.container { - background-color: theme.$header-color; - min-height: 10px; - padding: 10px 20px; - box-sizing: border-box; - - .category-navigation { - width: 100%; - height: 30px; - display: flex; - margin-bottom: 15px; - - button { - width: 100%; - margin-right: 5px; - border-radius: 5px; - text-decoration: none; - display: flex; - - span { - margin: auto; - color: white; - } - } - - button:not(.active) { - background-color: #4c515e; - } - - button:hover, - button:focus { - filter: brightness(50%); - } - - button:active { - // display: none; - } - } - - .setting-row { - display: flex; - flex-direction: row; - min-height: 30px; - padding: 5px; - box-sizing: border-box; - - .setting-title { - font-size: 18px; - margin: auto auto auto 0; - } - - .setting-toggleable { - min-width: 150px; - display: flex; - margin: auto 0 auto auto; - - .spinner { - margin: auto 0 auto auto; - animation: spin 1s; - animation-timing-function: linear; - animation-iteration-count: infinite; - } - - input { - padding: 0; - margin: auto 0 auto auto; - height: 25px; - width: 100px; - box-sizing: border-box; - text-align: center; - border-radius: 10px; - } - - input[type="text"] { - width: 350px; - } - - button { - width: 125px; - margin-left: auto; - height: 30px; - } - } - } - .setting-row:nth-child(even) { - background-color: #1c1c1c; - } - .setting-row:nth-child(odd) { - background-color: #191919; - } -} -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(-360deg); - } -} diff --git a/frontend/public/css/blog-list.css b/frontend/public/css/blog-list.css deleted file mode 100644 index c561f5c..0000000 --- a/frontend/public/css/blog-list.css +++ /dev/null @@ -1,128 +0,0 @@ -.blog-admin { - width: 100%; - background-color: #222; - margin-bottom: 20px; - padding: 5px; - box-sizing: border-box; -} -.blog-admin .horizontal-button-container a { - background-color: #00367b; - color: white; - text-decoration: none; - padding: 5px 10px; - box-sizing: border-box; - border-radius: 5px; -} - -.search-area { - width: 100%; - height: 30px; - margin-bottom: 10px; - display: flex; - flex-direction: row; -} -.search-area input { - width: 50%; - background-color: black; - border: 0; - outline: 0; - border-radius: 5px; - color: white; - height: 100%; - text-indent: 5px; - margin: 0 10px 0 auto; -} -.search-area button { - height: 100%; - min-width: 100px; - margin: 0 auto 0 0; -} - -.blog-entry { - width: 100%; - display: grid; - grid-template-columns: 150px auto; - grid-gap: 10px; - margin-bottom: 10px; -} -.blog-entry .thumbnail { - width: 150px; - height: 150px; -} -.blog-entry .thumbnail img { - height: 100%; - width: 100%; -} -.blog-entry .blog-info { - display: flex; - flex-direction: column; -} -.blog-entry .blog-info .blog-title { - font-size: 20px; - border-bottom: 1px solid #9f9f9f; - display: flex; -} -.blog-entry .blog-info .blog-title a { - color: white; - text-decoration: none; -} -.blog-entry .blog-info .blog-title .author { - color: #9f9f9f; - font-style: italic; - margin-left: auto; - font-size: 16px; -} -.blog-entry .blog-info .blog-description { - color: #9f9f9f; - margin-top: 10px; - max-height: 100px; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - word-break: none; - overflow: hidden; - text-align: left; -} -.blog-entry .blog-info .blog-action { - display: flex; - flex-direction: row; - margin-top: auto; -} -.blog-entry .blog-info .blog-action .date { - font-size: 16px; - color: gray; - margin-right: auto; -} -.blog-entry .blog-info .blog-action a { - color: white; - font-style: italic; -} - -.blog-entry:last-of-type { - margin-bottom: inherit; -} - -@media screen and (max-width: 500px) { - .search-area { - height: 60px; - flex-direction: column; - } - .search-area input { - width: 100%; - height: 30px; - margin: 0; - } - .search-area button { - height: 30px; - width: 100%; - margin: 0; - } - .page .blog-entry { - grid-template-columns: 75px auto; - margin-bottom: 20px; - } - .page .blog-entry .thumbnail { - width: 75px; - height: 75px; - } -} \ No newline at end of file diff --git a/frontend/public/css/blog-list.scss b/frontend/public/css/blog-list.scss deleted file mode 100644 index 08d0736..0000000 --- a/frontend/public/css/blog-list.scss +++ /dev/null @@ -1,142 +0,0 @@ -$quiet-text: #9f9f9f; - -.blog-admin { - width: 100%; - background-color: #222; - margin-bottom: 20px; - padding: 5px; - box-sizing: border-box; - - .horizontal-button-container { - a { - background-color: #00367b; - color: white; - text-decoration: none; - padding: 5px 10px; - box-sizing: border-box; - border-radius: 5px; - } - } -} - -.search-area { - width: 100%; - height: 30px; - margin-bottom: 10px; - display: flex; - flex-direction: row; - - input { - width: 50%; - background-color: black; - border: 0; - outline: 0; - border-radius: 5px; - color: white; - height: 100%; - text-indent: 5px; - margin: 0 10px 0 auto; - } - - button { - height: 100%; - min-width: 100px; - margin: 0 auto 0 0; - } -} - -.blog-entry { - width: 100%; - display: grid; - grid-template-columns: 150px auto; - grid-gap: 10px; - margin-bottom: 10px; - - .thumbnail { - width: 150px; - height: 150px; - img { - height: 100%; - width: 100%; - } - } - - .blog-info { - display: flex; - flex-direction: column; - .blog-title { - font-size: 20px; - border-bottom: 1px solid $quiet-text; - display: flex; - a { - color: white; - text-decoration: none; - } - - .author { - color: $quiet-text; - font-style: italic; - margin-left: auto; - font-size: 16px; - } - } - .blog-description { - color: $quiet-text; - margin-top: 10px; - max-height: 100px; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - word-break: none; - overflow: hidden; - text-align: left; - } - - .blog-action { - display: flex; - flex-direction: row; - margin-top: auto; - - .date { - font-size: 16px; - color: gray; - margin-right: auto; - } - a { - color: white; - font-style: italic; - } - } - } -} - -.blog-entry:last-of-type { - margin-bottom: inherit; -} - -@media screen and (max-width: 500px) { - .search-area { - height: 60px; - flex-direction: column; - input { - width: 100%; - height: 30px; - margin: 0; - } - button { - height: 30px; - width: 100%; - margin: 0; - } - } - .page { - .blog-entry { - grid-template-columns: 75px auto; - margin-bottom: 20px; - .thumbnail { - width: 75px; - height: 75px; - } - } - } -} diff --git a/frontend/public/css/blogNew.css b/frontend/public/css/blogNew.css deleted file mode 100644 index 706206c..0000000 --- a/frontend/public/css/blogNew.css +++ /dev/null @@ -1,192 +0,0 @@ -.e-header { - width: 100%; - display: grid; - grid-template-columns: 150px auto; - background-color: #222; - padding: 10px; - box-sizing: border-box; - grid-gap: 20px; - margin-bottom: 10px; -} -.e-header .e-thumbnail { - height: 150px; -} -.e-header .e-thumbnail img { - height: 100%; - width: 100%; - -o-object-fit: cover; - object-fit: cover; -} -.e-header .e-description { - width: 100%; - display: flex; - flex-direction: column; -} -.e-header .e-description input { - margin-bottom: 5px; -} -.e-header .e-description textarea { - color: #ccc; - font-size: 16px; - width: 100%; - flex-grow: 1; -} - -.e-image-area { - background-color: #222; - min-height: 200px; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); - grid-template-rows: auto auto; - grid-gap: 4px; - padding: 5px; - box-sizing: border-box; - width: 100%; - margin-bottom: 10px; -} -.e-image-area .placeholder { - margin: auto; - grid-row: 1/-1; - grid-column: 1/-1; -} -.e-image-area .image { - height: 100px; - aspect-ratio: 16/9; - margin: auto; - display: flex; - position: relative; -} -.e-image-area .image img { - max-height: 100%; - max-width: 100%; - margin: auto; -} -.e-image-area .image div { - position: absolute; - right: 0; - padding: 5px 10px; - box-sizing: border-box; - background-color: darkred; - cursor: pointer; -} - -.e-content { - background-color: #222; - 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-tags { - min-height: 40px; - width: 100%; - background-color: #222; - display: flex; - flex-direction: row; - padding: 5px; - box-sizing: border-box; - margin-bottom: 1rem; -} -.e-tags input { - width: 100%; -} - -.e-settings { - min-height: 40px; - width: 100%; - background-color: #222; - display: flex; - flex-direction: row; - padding: 5px; - box-sizing: border-box; - margin-bottom: 1rem; -} -.e-settings .publish-date { - display: flex; -} -.e-settings .publish-date div { - margin: auto 10px auto auto; -} -.e-settings .publish-date input { - margin-right: 5px; -} -.e-settings input, -.e-settings textarea { - width: 200px; -} -.e-settings .horizontal-buttons { - width: 100%; - display: flex; - flex-direction: row; - flex-wrap: wrap; -} -.e-settings .horizontal-buttons button { - width: 200px; - height: 40px; -} - -input, -textarea { - width: 100%; - padding: 5px; - box-sizing: border-box; - margin: 0; - border: 0; - background-color: black; - color: white; - font-size: 18px; - resize: vertical; -} \ No newline at end of file diff --git a/frontend/public/css/blogNew.scss b/frontend/public/css/blogNew.scss deleted file mode 100644 index 7b3d0a9..0000000 --- a/frontend/public/css/blogNew.scss +++ /dev/null @@ -1,211 +0,0 @@ -$background-body: #222; - -.e-header { - width: 100%; - display: grid; - grid-template-columns: 150px auto; - background-color: $background-body; - padding: 10px; - box-sizing: border-box; - grid-gap: 20px; - margin-bottom: 10px; - - .e-thumbnail { - height: 150px; - - img { - height: 100%; - width: 100%; - object-fit: cover; - } - } - - .e-description { - width: 100%; - display: flex; - flex-direction: column; - - input { - margin-bottom: 5px; - } - - textarea { - color: #ccc; - font-size: 16px; - width: 100%; - flex-grow: 1; - } - } -} - -.e-image-area { - background-color: $background-body; - min-height: 200px; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); - grid-template-rows: auto auto; - grid-gap: 4px; - padding: 5px; - box-sizing: border-box; - width: 100%; - margin-bottom: 10px; - - .placeholder { - margin: auto; - grid-row: 1 / -1; - grid-column: 1 / -1; - } - - .image { - height: 100px; - aspect-ratio: 16/9; - margin: auto; - display: flex; - position: relative; - - img { - max-height: 100%; - max-width: 100%; - margin: auto; - } - - div { - position: absolute; - right: 0; - padding: 5px 10px; - box-sizing: border-box; - background-color: darkred; - cursor: pointer; - } - } -} - -.e-content { - 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; - } -} - -.e-tags { - min-height: 40px; - width: 100%; - background-color: $background-body; - display: flex; - flex-direction: row; - padding: 5px; - box-sizing: border-box; - margin-bottom: 1rem; - - input { - width: 100%; - } -} - -.e-settings { - min-height: 40px; - width: 100%; - background-color: $background-body; - display: flex; - flex-direction: row; - padding: 5px; - box-sizing: border-box; - margin-bottom: 1rem; - .publish-date { - display: flex; - div { - margin: auto 10px auto auto; - } - input { - margin-right: 5px; - } - } - - input, - textarea { - width: 200px; - } - - .horizontal-buttons { - width: 100%; - display: flex; - flex-direction: row; - flex-wrap: wrap; - - button { - width: 200px; - height: 40px; - } - } -} - -input, -textarea { - width: 100%; - padding: 5px; - box-sizing: border-box; - margin: 0; - border: 0; - background-color: black; - color: white; - font-size: 18px; - resize: vertical; -} diff --git a/frontend/public/css/blogSingle.css b/frontend/public/css/blogSingle.css deleted file mode 100644 index 23c5e97..0000000 --- a/frontend/public/css/blogSingle.css +++ /dev/null @@ -1,68 +0,0 @@ -.page .title { - background-color: #222; - padding: 10px; - box-sizing: border-box; - font-size: 24px; -} -.page .image-container { - width: 100%; - margin-bottom: 4px; -} -.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; - place-content: space-around; -} -.page .side-by-side .image-container { - padding: 5px; - box-sizing: border-box; - 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; -} -.page h2 { - width: 50%; - border-bottom: 1px solid #777; -} -.page h3 { - width: 25%; - border-bottom: 1px solid #777; -} -.page h4 { - width: 20%; - border-bottom: 1px solid #777; -} -.page a { - color: white; -} - -@media screen and (max-width: 500px) { - .page .side-by-side .image-container { - width: 100%; - } -} \ No newline at end of file diff --git a/frontend/public/css/blogSingle.scss b/frontend/public/css/blogSingle.scss deleted file mode 100644 index 699f09d..0000000 --- a/frontend/public/css/blogSingle.scss +++ /dev/null @@ -1,86 +0,0 @@ -.page { - .title { - background-color: #222; - padding: 10px; - box-sizing: border-box; - font-size: 24px; - } - - .image-container { - width: 100%; - margin-bottom: 4px; - - img { - width: 100%; - } - } - - .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; - place-content: space-around; - - .image-container { - padding: 5px; - box-sizing: border-box; - width: 50%; - margin-bottom: 0; - } - - .video-embed { - width: 50%; - min-height: 280px; - padding: 5px; - box-sizing: border-box; - - iframe { - width: 100%; - height: 280px; - } - } - } - - h1 { - border-bottom: 1px solid #777; - } - - h2 { - width: 50%; - border-bottom: 1px solid #777; - } - h3 { - width: 25%; - border-bottom: 1px solid #777; - } - - h4 { - width: 20%; - border-bottom: 1px solid #777; - } - - a { - color: white; - } -} - -@media screen and (max-width: 500px) { - .page { - .side-by-side { - .image-container { - width: 100%; - } - } - } -} diff --git a/frontend/public/css/signin.css b/frontend/public/css/signin.css deleted file mode 100644 index efaf286..0000000 --- a/frontend/public/css/signin.css +++ /dev/null @@ -1,217 +0,0 @@ -body { - margin: 0; - background-color: #111; - color: white; - font-family: Verdana, Geneva, Tahoma, sans-serif; -} - -.header { - background-color: #222; - width: 100%; - height: 50px; - display: flex; - flex-direction: row; - padding: 0 10px; - box-sizing: border-box; - margin-bottom: 1rem; -} -.header .left { - margin: auto auto auto 0; - font-size: 20px; - height: 100%; -} -.header .left a { - width: inherit; -} -.header .right { - margin: auto 0 auto auto; - height: 100%; - display: flex; - flex-direction: row; -} -.header .right a:hover, -.header .right a:focus { - background-color: #333; -} -.header a { - height: 100%; - width: 130px; - margin: auto 0; - display: flex; - text-decoration: none; - transition: background-color ease-in-out 0.1s; -} -.header a div { - margin: auto; - color: white; -} - -button { - background-color: #1c478a; - border: 0; - border-radius: 5px; - color: white; - cursor: pointer; -} - -button:hover, -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; - margin: 0 auto; -} -.page .horizontal-button-container { - display: flex; - flex-direction: row; -} -.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; - background-color: #222; - display: flex; - flex-direction: column; - padding: 0 20px 20px 20px; - box-sizing: border-box; - border-radius: 5px; -} -.center-modal .modal-title { - text-align: center; - font-size: 26px; - margin-top: 10px; -} -.center-modal .input-line { - display: flex; - flex-direction: column; - margin-bottom: 20px; -} -.center-modal .input-line div { - margin-bottom: 5px; - font-size: 18px; -} -.center-modal .input-line input { - background-color: #0d0d0d; - border: 0; - padding: 5px; - box-sizing: border-box; - color: white; -} -.center-modal .horizontal-button-container { - flex-direction: row-reverse !important; -} -.center-modal .horizontal-button-container * { - width: 100% !important; -} -.center-modal .horizontal-button-container a, -.center-modal .horizontal-button-container button { - 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 deleted file mode 100644 index a46469f..0000000 --- a/frontend/public/css/signin.scss +++ /dev/null @@ -1,57 +0,0 @@ -@use "theme"; - -.center-modal { - margin: auto; - width: 400px; - background-color: theme.$header-color; - display: flex; - flex-direction: column; - padding: 0 20px 20px 20px; - box-sizing: border-box; - border-radius: 5px; - - .modal-title { - text-align: center; - font-size: 26px; - margin-top: 10px; - } - - .input-line { - display: flex; - flex-direction: column; - margin-bottom: 20px; - - div { - margin-bottom: 5px; - font-size: 18px; - } - - input { - background-color: #0d0d0d; - border: 0; - padding: 5px; - box-sizing: border-box; - color: white; - } - } - - .horizontal-button-container { - flex-direction: row-reverse !important; - - * { - width: 100% !important; - } - - a, - button { - color: white; - display: flex; - width: min-content; - - span { - margin: auto; - text-align: center; - } - } - } -} diff --git a/frontend/public/css/theme.css b/frontend/public/css/theme.css deleted file mode 100644 index 39fe2c3..0000000 --- a/frontend/public/css/theme.css +++ /dev/null @@ -1,168 +0,0 @@ -body { - margin: 0; - background-color: #111; - color: white; - font-family: Verdana, Geneva, Tahoma, sans-serif; -} - -.header { - background-color: #222; - width: 100%; - height: 50px; - display: flex; - flex-direction: row; - padding: 0 10px; - box-sizing: border-box; - margin-bottom: 1rem; -} -.header .left { - margin: auto auto auto 0; - font-size: 20px; - height: 100%; -} -.header .left a { - width: inherit; -} -.header .right { - margin: auto 0 auto auto; - height: 100%; - display: flex; - flex-direction: row; -} -.header .right a:hover, -.header .right a:focus { - background-color: #333; -} -.header a { - height: 100%; - width: 130px; - margin: auto 0; - display: flex; - text-decoration: none; - transition: background-color ease-in-out 0.1s; -} -.header a div { - margin: auto; - color: white; -} - -button { - background-color: #1c478a; - border: 0; - border-radius: 5px; - color: white; - cursor: pointer; -} - -button:hover, -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; - margin: 0 auto; -} -.page .horizontal-button-container { - display: flex; - flex-direction: row; -} -.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%; - } -} \ No newline at end of file diff --git a/frontend/public/css/theme.scss b/frontend/public/css/theme.scss deleted file mode 100644 index 4695f31..0000000 --- a/frontend/public/css/theme.scss +++ /dev/null @@ -1,186 +0,0 @@ -$header-color: #222; -$button-generic: #1c478a; - -body { - margin: 0; - background-color: #111; - color: white; - font-family: Verdana, Geneva, Tahoma, sans-serif; -} - -.header { - background-color: $header-color; - width: 100%; - height: 50px; - display: flex; - flex-direction: row; - padding: 0 10px; - box-sizing: border-box; - margin-bottom: 1rem; - - .left { - margin: auto auto auto 0; - font-size: 20px; - height: 100%; - - a { - width: inherit; - } - } - .right { - margin: auto 0 auto auto; - height: 100%; - display: flex; - flex-direction: row; - - a:hover, - a:focus { - background-color: #333; - } - } - a { - height: 100%; - width: 130px; - margin: auto 0; - display: flex; - text-decoration: none; - transition: background-color ease-in-out 0.1s; - - div { - margin: auto; - color: white; - } - } -} - -button { - background-color: $button-generic; - border: 0; - border-radius: 5px; - color: white; - cursor: pointer; -} - -button:hover, -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; - margin: 0 auto; - - .horizontal-button-container { - display: flex; - flex-direction: row; - - button, - a { - width: 120px; - min-height: 30px; - text-decoration: none; - // background-color: #222; - display: flex; - margin-right: 5px; - - span { - margin: auto; - } - } - - button:last-of-type, - a:last-of-type { - margin-right: 0; - } - } - - .blog-admin { - margin-bottom: 10px; - } - - .pagination { - display: flex; - flex-direction: row; - width: 100%; - margin: 0 auto; - - a { - height: 40px; - width: 150px; - margin-right: 5px; - display: flex; - text-decoration: none; - background-color: #222; - - span { - margin: auto; - color: white; - font-size: 20px; - } - } - a:last-of-type { - margin-right: 0; - margin-left: 5px; - } - - a.disabled { - filter: brightness(50%); - } - - .page-list { - flex-grow: 1; - display: flex; - flex-direction: row; - margin-bottom: 50px; - - a { - width: 40px; - height: 40px; - display: flex; - text-decoration: none; - background-color: #222; - border-radius: 10px; - margin: 0 10px 0 0; - - span { - margin: auto; - color: white; - } - } - - a:first-of-type { - margin: auto 10px auto auto; - } - a:last-of-type { - margin: auto auto auto 0; - } - - a.active { - background-color: #993d00; - } - } - } -} - -.hidden { - display: none !important; -} -@media screen and (max-width: 1010px) { - .page { - width: 95%; - } -} diff --git a/frontend/public/js/admin.js b/frontend/public/js/admin.js deleted file mode 100644 index 3bfa6fd..0000000 --- a/frontend/public/js/admin.js +++ /dev/null @@ -1,72 +0,0 @@ -async function toggleState(setting_name, new_value, element_id) { - // Show spinner - qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden"); - - const form = { - setting_name: setting_name, - value: JSON.stringify(new_value), - }; - - const response = await request("/setting", "POST", form); - - qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.add("hidden"); - - // TODO: On failure, notify the user - // Check response for errors - if (response.body.success) { - // Update visual to reflect current setting - // Class - const add_class = new_value ? "good" : "bad"; - const remove_class = new_value ? "bad" : "good"; - qs(`#${element_id}`).classList.remove(remove_class); - qs(`#${element_id}`).classList.add(add_class); - - // Text - const new_text = new_value ? "Enabled" : "Disabled"; - qs(`#${element_id}`).children[0].innerText = new_text; - - // Function - const add_function = new_value ? `toggleState('${setting_name}', false, this.id)` : `toggleState('${setting_name}', true, this.id)`; - qs(`#${element_id}`).removeAttribute("onclick"); - qs(`#${element_id}`).setAttribute("onclick", add_function); - } -} - -async function updateValue(key_pressed, setting_name, new_value, element_id) { - if (key_pressed !== 13) return; - // Show spinner - qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden"); - - const form = { - setting_name: setting_name, - value: new_value, - }; - - const response = await request("/setting", "POST", form); - - qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.add("hidden"); - - // TODO: On failure, notify the user - // Check response for errors - if (response.body.success) { - } -} - -function toggleActiveCategory(category_id) { - // Pages ---------------- - // Hide all pages - qsa(".category-page").forEach((page) => { - page.classList.add("hidden"); - }); - // Show requested page - qs(`#${category_id}`).classList.remove("hidden"); - - // Navigation bar ------- - // Unactive all buttons - qsa(".category-navigation button").forEach((btn) => { - btn.classList.remove("active"); - }); - // Active current page - qs(`#${category_id}-nav-btn`).classList.add("active"); - qs(`#${category_id}-nav-btn`).blur(); // Unfocus the button -} diff --git a/frontend/public/js/blogSingle.js b/frontend/public/js/blogSingle.js deleted file mode 100644 index 2133cb8..0000000 --- a/frontend/public/js/blogSingle.js +++ /dev/null @@ -1,8 +0,0 @@ -const delete_post_btn = qs("#delete-post"); - -if (delete_post_btn) { - delete_post_btn.addEventListener("click", async () => { - let req = await request("/api/web/blog", "DELETE", { id: window.location.href.split("/")[4] }); - if (req.body.success) location.reload(); - }); -} diff --git a/frontend/public/js/login.js b/frontend/public/js/login.js deleted file mode 100644 index 302a288..0000000 --- a/frontend/public/js/login.js +++ /dev/null @@ -1,17 +0,0 @@ -async function requestLogin() { - const account_information = { - username: qs("#username").value, - password: qs("#password").value, - }; - - const account_response = await request("/login", "POST", account_information); - - // Check response for errors - - // If success, return to account - console.log(account_response); - - if (account_response.body.success) { - location.href = "/"; - } -} diff --git a/frontend/public/js/newBlog.js b/frontend/public/js/newBlog.js deleted file mode 100644 index 0a1245a..0000000 --- a/frontend/public/js/newBlog.js +++ /dev/null @@ -1,257 +0,0 @@ -const blog_id = window.location.href.split("/")[4]; - -let existing_images = []; -let pending_images = []; -let pending_thumbnail = {}; - -const thumbnail_area = qs(".e-thumbnail"); -const image_area = qs(".e-image-area"); -const text_area = qs(".e-content textarea"); - -// Style -function stylizeDropArea(element) { - // Drag over start - element.addEventListener("dragover", (e) => { - e.preventDefault(); - e.target.classList.add("drag-over"); - }); - - // Drag over leave - element.addEventListener("dragleave", (e) => { - e.target.classList.remove("drag-over"); - }); - // Do nothing on drop - element.addEventListener("drop", (e) => { - e.preventDefault(); - }); -} - -// 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 -text_area.addEventListener("input", (e) => { - text_area.style.height = text_area.scrollHeight + "px"; - text_area.style.minHeight = e.target.scrollHeight + "px"; -}); - -stylizeDropArea(thumbnail_area); -stylizeDropArea(image_area); - -// Upload an image to the blog post -image_area.addEventListener("drop", async (e) => { - const files = e.dataTransfer.files; - - for (let i = 0; i < files.length; i++) { - // Each dropped image will be stored in this formatted object - const image_object = { - id: crypto.randomUUID(), - data_blob: new Blob([await files[i].arrayBuffer()]), - content_type: files[i].type, - }; - - // Add the image's data to the list - pending_images.push(image_object); - } - - // Update the displayed images - updateImages(); -}); - -// Upload an image to the blog post -thumbnail_area.addEventListener("drop", async (e) => { - const file = e.dataTransfer.files[0]; - - // The thumbnail will be stored in this formatted object - const image_object = { - id: crypto.randomUUID(), - data_blob: new Blob([await file.arrayBuffer()]), - content_type: file.type, - }; - - // Add the image's data to the list - pending_thumbnail = image_object; - - // Update the visible thumbnail - qs(".e-thumbnail img").src = URL.createObjectURL(image_object.data_blob); - - // Update the displayed images - updateImages(); -}); - -// Publish or Update a blog post with the new data -async function publishBlog(unlisted, edit) { - // Format our request we will send to the server - let form_data = { - title: qs("#title").value, - description: qs("#description").value, - content: qs("#content").value, - visibility: unlisted ? "UNLISTED" : "PUBLISHED", - tags: [], - date: qs("#date").value, - time: qs("#time").value, - }; - - // Get our tags, trim them, then shove them into an array - const tags_value = qs("#tags").value || ""; - if (tags_value.length) { - let tags_array = qs("#tags").value.split(","); - tags_array.forEach((tag) => form_data.tags.push(tag.trim())); - } - - // If we have a thumbnail, read the thumbnail image and store it - if (pending_thumbnail.data_blob) { - form_data.thumbnail = { ...pending_thumbnail, data_blob: await _readFile(pending_thumbnail.data_blob) }; - } - - // We have images to upload - if (pending_images.length + existing_images.length > 0) { - // Initialize the image array - form_data.images = []; - - // Read the image, convert to base64, update the existing variable with the base64 - for (let i = 0; pending_images.length > i; i++) { - form_data.images.push({ ...pending_images[i], data_blob: await _readFile(pending_images[i].data_blob) }); - } - } - - // We are making edits to a post, not uploading a new one - if (edit) { - form_data.id = blog_id; - } - - // Send the request! - const method = edit ? "PATCH" : "post"; - - const res = await request("/api/web/blog", method, form_data); - - if (res.body.success) { - window.location.href = `/blog/${res.body.blog_id || blog_id}`; - } -} - -// Send a request to delete an image -async function deleteImage(image_id) { - const res = await request("/api/web/blog/image", "delete", { id: image_id, parent: blog_id, parent_type: "blog" }); - if (res.body.success) { - // Remove from existing images (If it exists) - let image = existing_images.find((item) => item.id === image_id); - if (image) existing_images.splice(existing_images.indexOf(image), 1); - - image = pending_images.find((item) => item.id === image_id); - if (image) pending_images.splice(pending_images.indexOf(image), 1); - - updateImages(); - } -} - -// 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); - }); -} - -function customDragString() { - const images = qsa(".e-image-area .image img"); - - images.forEach((image) => { - image.addEventListener("dragstart", (event) => { - event.dataTransfer.setData("text/plain", event.target.getAttribute("data-image_id")); - }); - }); -} - -function updateImages() { - const image_div = (img_id, img_url) => ``; - - // Clear existing listings - qsa(".e-image-area .image").forEach((entry) => entry.remove()); - - // Clear placeholder text - 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)); - }); - - // Add new entries based on saved list - pending_images.forEach((image) => { - image_area.insertAdjacentHTML("beforeend", image_div(image.id, URL.createObjectURL(image.data_blob))); - }); - - 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(); - - // Get the custom string from the drag data - const customString = `\{image:${event.dataTransfer.getData("text/plain")}\}\n`; - - // Insert the custom string at the cursor position - const selectionStart = text_area.selectionStart; - const selectionEnd = text_area.selectionEnd; - - const textBefore = text_area.value.substring(0, selectionStart); - const textAfter = text_area.value.substring(selectionEnd); - - const updatedText = textBefore + customString + textAfter; - - text_area.value = updatedText; - - // Set the cursor position after the custom string - const newPosition = selectionStart + customString.length; - text_area.setSelectionRange(newPosition, newPosition); -}); - -// Load the existing images into our existing_images variable -qsa(".e-image-area img").forEach((image) => { - existing_images.push({ id: image.getAttribute("data-image_id"), url: image.src }); -}); - -updateImages(); diff --git a/frontend/views/admin.ejs b/frontend/views/admin.ejs deleted file mode 100644 index ff02d16..0000000 --- a/frontend/views/admin.ejs +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - <%= website_name %> | Administration - - - <%- include("partials/header.ejs", {selected: 'home'}) %> - -
-
-
- - - - -
-
- - <%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'ACCOUNT_REGISTRATION', name_pretty: 'Account registration', enabled: - settings.ACCOUNT_REGISTRATION}}) %> - - <%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'HIDE_LOGIN', name_pretty: 'Hide Login', enabled: settings.HIDE_LOGIN}}) %> - - <%- include("partials/admin-setting-number.ejs", {setting: {name:'USER_MINIMUM_PASSWORD_LENGTH', name_pretty: 'Minimum Password Length', value: - settings.USER_MINIMUM_PASSWORD_LENGTH}}) %> -
- - - - - -
-
- - <%- include("partials/footer.ejs") %> - - - - diff --git a/frontend/views/author.ejs b/frontend/views/author.ejs deleted file mode 100644 index e423cca..0000000 --- a/frontend/views/author.ejs +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - <%= website_name %> | <%= blog_post.title %> - - - <%- include("partials/header.ejs", {selected: 'home'}) %> - -
- <%if(logged_in_user) {%> -
- -
- - <%}%> -
<%= blog_post.title%>
- - <%- blog_post.content %> -
- - <%- include("partials/footer.ejs") %> - - - - diff --git a/frontend/views/blogList.ejs b/frontend/views/blogList.ejs deleted file mode 100644 index 97b2379..0000000 --- a/frontend/views/blogList.ejs +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - <%= website_name %> | Home - - - <%- include("partials/header.ejs", {selected: 'home'}) %> - -
- <%if(logged_in_user) {%> <%- include("partials/blog-admin.ejs") %> <%}%> - -
- -
- - <% for(post of blog_list) { %> - - <%- include("partials/blog-entry.ejs", {post:post}) %> - - <% } %> <%- include("partials/pagination.ejs") %> -
- - <%- include("partials/footer.ejs") %> - - - - diff --git a/frontend/views/blogNew.ejs b/frontend/views/blogNew.ejs deleted file mode 100644 index a446a0d..0000000 --- a/frontend/views/blogNew.ejs +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - <%= website_name %> | New Blog - - - <%- include("partials/header.ejs", {selected: 'home'}) %> - -
-
-
- <%if(existing_blog?.thumbnail) {%> - - <%} else {%> - - <% } %> -
-
- - -
-
- -
- <% if(existing_blog.raw_images?.length) { %> <% for (image in existing_blog.raw_images) {%> -
- - -
- <%}%> <% } else {%> -
Drop images here
- <% } %> -
- -
-
- -
- - -
-
- -
-
- -
-
-
-
Publish On
- <% if(existing_blog.publish_date) {%> - - <%} else { %> - - <% } %> - - <% if(existing_blog.publish_date) {%> - - <%} else { %> - - <% } %> -
-
-
-
- - <% if(existing_blog.id){%> - - <% } else {%> - - <% } %> -
-
-
- - <%- include("partials/footer.ejs") %> - - - - diff --git a/frontend/views/blogSingle.ejs b/frontend/views/blogSingle.ejs deleted file mode 100644 index d458b3f..0000000 --- a/frontend/views/blogSingle.ejs +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - <%= website_name %> | <%= blog_post.title %> - - - <%- include("partials/header.ejs", {selected: 'home'}) %> - -
- - - <%if(logged_in_user) {%> - - - <%}%> -
<%= blog_post.title%>
- - <%- blog_post.content %> -
- - <%- include("partials/footer.ejs") %> - - - - diff --git a/frontend/views/index.ejs b/frontend/views/index.ejs deleted file mode 100644 index 89f6e31..0000000 --- a/frontend/views/index.ejs +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - <%= website_name %> | Home - - - <%- include("partials/header.ejs", {selected: 'home'}) %> - -
- <%- include("partials/blog-entry.ejs", {thumbnail: '/img/.dev/square.png', title: 'Title', description: 'Description', author: 'Author'}) %> -
- - <%- include("partials/footer.ejs") %> - - - - diff --git a/frontend/views/login.ejs b/frontend/views/login.ejs deleted file mode 100644 index 3236416..0000000 --- a/frontend/views/login.ejs +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - <%= website_name %> | Login - - - <%- include("partials/header.ejs", {selected: 'home'}) %> - -
-
- -
-
Username
- -
-
-
Password
- -
-
- - Register -
-
-
- - <%- include("partials/footer.ejs") %> - - - - diff --git a/frontend/views/partials/admin-setting-number.ejs b/frontend/views/partials/admin-setting-number.ejs deleted file mode 100644 index 7002e4f..0000000 --- a/frontend/views/partials/admin-setting-number.ejs +++ /dev/null @@ -1,15 +0,0 @@ -
-
<%=setting.name_pretty%>
-
- - -
-
diff --git a/frontend/views/partials/admin-setting-text.ejs b/frontend/views/partials/admin-setting-text.ejs deleted file mode 100644 index 989e806..0000000 --- a/frontend/views/partials/admin-setting-text.ejs +++ /dev/null @@ -1,15 +0,0 @@ -
-
<%=setting.name_pretty%>
-
- - -
-
diff --git a/frontend/views/partials/admin-setting-toggle.ejs b/frontend/views/partials/admin-setting-toggle.ejs deleted file mode 100644 index d9def8f..0000000 --- a/frontend/views/partials/admin-setting-toggle.ejs +++ /dev/null @@ -1,13 +0,0 @@ -
-
<%=setting.name_pretty%>
-
- - <% if (!setting.enabled) { %> - - <% } else { %> - - <%}%> -
-
diff --git a/frontend/views/partials/blog-admin.ejs b/frontend/views/partials/blog-admin.ejs deleted file mode 100644 index 405a0de..0000000 --- a/frontend/views/partials/blog-admin.ejs +++ /dev/null @@ -1,5 +0,0 @@ -
-
- New Post -
-
diff --git a/frontend/views/partials/blog-entry.ejs b/frontend/views/partials/blog-entry.ejs deleted file mode 100644 index 9062063..0000000 --- a/frontend/views/partials/blog-entry.ejs +++ /dev/null @@ -1,16 +0,0 @@ -
- - - -
- -
<%= post.description %>
-
-
<%= post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) %>
- Read this post -> -
-
-
diff --git a/frontend/views/partials/footer.ejs b/frontend/views/partials/footer.ejs deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/views/partials/header.ejs b/frontend/views/partials/header.ejs deleted file mode 100644 index 8199ca9..0000000 --- a/frontend/views/partials/header.ejs +++ /dev/null @@ -1,21 +0,0 @@ -
- -
- -
Blog
-
- <% if (logged_in_user) { %> - -
Profile
-
- <% } else {%> <% if(!settings.HIDE_LOGIN) {%> - -
Login
-
- <% } %> <% } %> -
-
diff --git a/frontend/views/partials/pagination.ejs b/frontend/views/partials/pagination.ejs deleted file mode 100644 index 86d8f3d..0000000 --- a/frontend/views/partials/pagination.ejs +++ /dev/null @@ -1,29 +0,0 @@ - diff --git a/frontend/views/register.ejs b/frontend/views/register.ejs deleted file mode 100644 index cccfff9..0000000 --- a/frontend/views/register.ejs +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - <%= website_name %> | Register - - - <%- include("partials/header.ejs", {selected: 'home'}) %> - -
-
- -
-
Username
- -
-
-
Password
- -
-
- - Login -
-
-
- - <%- include("partials/footer.ejs") %> - - - - diff --git a/frontend/views/themes/default/css/author.css b/frontend/views/themes/default/css/author.css new file mode 100644 index 0000000..dafda4b --- /dev/null +++ b/frontend/views/themes/default/css/author.css @@ -0,0 +1,73 @@ +.page .page-center { + display: flex; + flex-direction: row; + min-height: 50px; + 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%; + background-color: white; + min-height: 50px; + box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px; + 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 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; + display: flex; +} +.page .page-center .about .profile-picture img { + margin: auto; + max-height: 200px; + max-width: 200px; +} +.page .page-center .about .displayname { + font-size: 1.4rem; + text-align: center; +} +.page .page-center .about .stat { + color: gray; + font-size: 1rem; + text-align: center; + margin-top: 0.5rem; +} +.page .page-center .about .sociallist { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 3rem; +} +.page .page-center .about .sociallist .link { + 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 new file mode 100644 index 0000000..a66b16f --- /dev/null +++ b/frontend/views/themes/default/css/author.scss @@ -0,0 +1,81 @@ +.page .page-center { + display: flex; + flex-direction: row; + min-height: 50px; + 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)); + background-color: white; + min-height: 50px; + 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 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; + display: flex; + + img { + margin: auto; + max-height: 200px; + max-width: 200px; + } + } + + .displayname { + font-size: 1.4rem; + text-align: center; + } + + .stat { + color: gray; + font-size: 1rem; + text-align: center; + margin-top: 0.5rem; + } + + .sociallist { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 3rem; + + .link { + text-decoration: none; + color: black; + margin: auto; + } + } + } +} + +.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 new file mode 100644 index 0000000..a74af58 --- /dev/null +++ b/frontend/views/themes/default/css/generic.css @@ -0,0 +1,252 @@ +body { + background-color: #f9fafc; + margin: 0; + font-family: Verdana, Geneva, Tahoma, sans-serif; +} + +.header { + width: 100%; + height: 50px; + color: black; +} +.header .page-center { + display: flex; + flex-direction: row; + height: 100%; +} +.header .logo { + display: flex; + height: 100%; + width: -moz-fit-content; + width: fit-content; + color: black; + text-decoration: none; +} +.header .logo .logo-icon { + height: 100%; + aspect-ratio: 1/1; +} +.header .logo .logo-icon::before { + background-image: url("../img/news.svg"); + width: 30px; + height: 30px; +} +.header .logo .logo-title { + font-size: 2rem; + text-align: center; + margin: auto; +} +.header .navigation { + margin: 0 0 0 auto; + height: 100%; + display: flex; + flex-direction: row; +} +.header .navigation a { + height: 100%; + display: flex; + flex-direction: row; + padding: 1rem; + box-sizing: border-box; + color: black; + text-decoration: none; +} +.header .navigation a span { + margin: auto; +} + +.page-center { + width: 1080px; + max-width: 1080px; + margin: 0 auto; + display: flex; + flex-direction: column; +} + +.page { + min-height: 700px; +} + +.button { + background-color: #0072ff; + border-radius: 5px; + color: white; + 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; + filter: brightness(80%); +} + +.button.bad { + background-color: #e70404; +} + +.button.caution { + background-color: #6d6d6c; +} + +.button.disabled { + filter: contrast(50%); + filter: brightness(50%); + cursor: default; +} + +.atom-feed::before { + background-image: url("/img/rss.svg"); +} + +.json::before { + background-image: url("/img/json.svg"); +} + +.footer .page-center { + display: flex; + flex-direction: row; + text-align: center; + margin-top: 2rem; +} +.footer .page-center * { + width: -moz-fit-content; + width: fit-content; + margin: auto; +} +.footer .page-center * a { + margin-bottom: 0.5rem; + color: black; + text-decoration: none; +} +.footer .page-center .resources { + display: flex; + flex-direction: column; + width: 33.3333333333%; +} +.footer .page-center .info { + display: flex; + flex-direction: column; + width: 33.3333333333%; +} + +.icon { + display: flex; +} + +.icon::before { + content: ""; + display: inline-block; + width: 20px; + height: 20px; + background-size: contain; + margin: auto 5px auto auto; +} + +.separator { + border-bottom: 2px solid #b3b3b3; + 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; +} + +.info .info-blip.visibility-flag { + margin-left: auto; + font-size: 0.9rem; + display: flex; + padding: 0 0.5rem; + border-radius: 5px; + border: 2px solid; +} +.info .info-blip.visibility-flag span { + margin: auto; + color: black; +} +.info .visibility-flag.published { + border-color: #21b525; + background-color: #a0ffa0; +} +.info .visibility-flag.unlisted { + border-color: #bec10f; + background-color: #e8ffa0; +} +.info .visibility-flag.private { + border-color: #c10f0f; + background-color: #ffd7d7; +} +.info .visibility-flag.draft { + border-color: black; + background-color: rgba(0, 0, 0, 0.0588235294); +} +.info .visibility-flag.scheduled { + border-color: #0f77c1; + background-color: #d7e9ff; + margin-left: inherit; +} + +@media screen and (max-width: 1280px) { + .page-center { + width: 95%; + } +} +@media screen and (max-width: 760px) { + .post-list-container .post-list { + width: 100%; + } + .tag-list { + display: none; + } +} \ No newline at end of file diff --git a/frontend/views/themes/default/css/generic.scss b/frontend/views/themes/default/css/generic.scss new file mode 100644 index 0000000..38e7865 --- /dev/null +++ b/frontend/views/themes/default/css/generic.scss @@ -0,0 +1,274 @@ +body { + background-color: #f9fafc; + margin: 0; + font-family: Verdana, Geneva, Tahoma, sans-serif; +} + +.header { + width: 100%; + height: 50px; + color: black; + + .page-center { + display: flex; + flex-direction: row; + height: 100%; + } + + .logo { + display: flex; + height: 100%; + width: fit-content; + color: black; + text-decoration: none; + + .logo-icon { + height: 100%; + aspect-ratio: 1/1; + } + + .logo-icon::before { + background-image: url("../img/news.svg"); + width: 30px; + height: 30px; + } + + .logo-title { + font-size: 2rem; + text-align: center; + margin: auto; + } + } + + .navigation { + margin: 0 0 0 auto; + height: 100%; + display: flex; + flex-direction: row; + + a { + height: 100%; + display: flex; + flex-direction: row; + padding: 1rem; + box-sizing: border-box; + color: black; + text-decoration: none; + + span { + margin: auto; + } + } + } +} + +.page-center { + width: 1080px; + max-width: 1080px; + margin: 0 auto; + display: flex; + flex-direction: column; +} + +.page { + min-height: 700px; +} + +.button { + background-color: #0072ff; + border-radius: 5px; + color: white; + 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; + filter: brightness(80%); +} +.button.bad { + background-color: #e70404; +} +.button.caution { + background-color: #6d6d6c; +} +.button.disabled { + filter: contrast(50%); + filter: brightness(50%); + cursor: default; +} + +.atom-feed::before { + background-image: url("/img/rss.svg"); +} +.json::before { + background-image: url("/img/json.svg"); +} + +.footer { + .page-center { + display: flex; + flex-direction: row; + text-align: center; + margin-top: 2rem; + + * { + width: fit-content; + margin: auto; + a { + margin-bottom: 0.5rem; + color: black; + text-decoration: none; + } + } + + .resources { + display: flex; + flex-direction: column; + width: calc(100% * (1 / 3)); + } + + .info { + display: flex; + flex-direction: column; + width: calc(100% * (1 / 3)); + } + } +} + +.icon { + display: flex; +} +.icon::before { + content: ""; + display: inline-block; + width: 20px; + height: 20px; + background-size: contain; + margin: auto 5px auto auto; +} + +.separator { + border-bottom: 2px solid #b3b3b3; + 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; + } +} +.info { + .info-blip.visibility-flag { + margin-left: auto; + font-size: 0.9rem; + display: flex; + padding: 0 0.5rem; + border-radius: 5px; + border: 2px solid; + + span { + margin: auto; + color: black; + } + } + + .visibility-flag.published { + border-color: #21b525; + background-color: #a0ffa0; + } + + .visibility-flag.unlisted { + border-color: #bec10f; + background-color: #e8ffa0; + } + + .visibility-flag.private { + border-color: #c10f0f; + background-color: #ffd7d7; + } + + .visibility-flag.draft { + border-color: black; + background-color: #0000000f; + } + + .visibility-flag.scheduled { + border-color: #0f77c1; + background-color: #d7e9ff; + margin-left: inherit; + } +} +@media screen and (max-width: 1280px) { + .page-center { + width: 95%; + } +} + +@media screen and (max-width: 760px) { + .post-list-container .post-list { + width: 100%; + } + .tag-list { + display: none; + } +} diff --git a/frontend/views/themes/default/css/index.css b/frontend/views/themes/default/css/index.css new file mode 100644 index 0000000..96da19d --- /dev/null +++ b/frontend/views/themes/default/css/index.css @@ -0,0 +1,195 @@ +.post-list-container { + margin-top: 2rem; + display: flex; + flex-direction: row; +} +.post-list-container .post-list { + width: 66.6666666667%; + min-height: 200px; +} +.post-list-container .post-list .post { + width: 100%; + background-color: #fff; + border-radius: 10px; + box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px; + min-height: 200px; + padding: 0.5rem 1rem; + box-sizing: border-box; + display: flex; + flex-direction: column; + margin-bottom: 1rem; +} +.post-list-container .post-list .post .title { + font-size: 2rem; + font-weight: 550; + margin-bottom: 0.5rem; + text-decoration: none; + color: black; +} +.post-list-container .post-list .post .authors { + margin-bottom: 1.5rem; +} +.post-list-container .post-list .post .authors a { + color: black; +} +.post-list-container .post-list .post .description { + margin-bottom: 1rem; +} +.post-list-container .post-list .post .badges { + margin-top: auto; + color: #414141; +} +.post-list-container .post-list .post .badges .tags { + display: flex; + flex-direction: row; + margin-bottom: 0.5rem; +} +.post-list-container .post-list .post .badges .info { + display: flex; + flex-direction: row; +} +.post-list-container .post-list .post .badges .info .info-blip { + margin-right: 1rem; +} +.post-list-container .post-list .post .badges .info .publish-date::before { + background-image: url("../img/calendar.svg"); /* Set the background image */ +} +.post-list-container .post-list .post .badges .info .reading-time::before { + background-image: url("../img/hourglass.svg"); /* Set the background image */ +} +.post-list-container .post-list .post .badges .info .word-count::before { + background-image: url("https://www.svgrepo.com/show/2460/cherry.svg"); /* Set the background image */ +} +.post-list-container .post-list.full { + width: 100%; +} +.post-list-container .tag-list { + width: 33.3333333333%; + max-width: 33.3333333333%; + min-height: 200px; + padding: 0 2rem; + box-sizing: border-box; +} +.post-list-container .tag-list .tag-header { + font-size: 1.2rem; + margin-bottom: 2rem; +} +.post-list-container .tag-list .list { + display: flex; + flex-direction: row; + overflow-wrap: break-word; + flex-wrap: wrap; + width: 100%; +} +.post-list-container .tag-list .list .tag { + height: -moz-fit-content; + height: fit-content; + background-color: lightgray; + margin-bottom: 0.5rem; + margin-right: 0.5rem; +} +.post-list-container .tag-list .list .tag::before { + display: none; +} + +.tag { + width: -moz-fit-content; + width: fit-content; + margin-right: 0.1rem; + padding: 0.2rem 0.3rem; + box-sizing: border-box; + border-radius: 4px; + color: black; + text-decoration: none; +} + +.tag::before { + background-image: url("../img/tag.svg"); /* Set the background image */ +} + +.pagination { + margin-top: auto; + width: 100%; + display: flex; + flex-direction: row; + height: 40px; +} +.pagination .left { + margin-right: 1rem; +} +.pagination .pages { + height: 100%; + margin: auto; + display: flex; + flex-direction: row; +} +.pagination .pages a { + display: flex; + height: 100%; + aspect-ratio: 1/1; + color: black; + text-decoration: none; +} +.pagination .pages a span { + margin: auto; +} +.pagination .pages a.active { + background-color: lightskyblue; + border-radius: 5px; +} +.pagination .right { + margin-left: 1rem; +} +.pagination .left, +.pagination .right { + padding: 0.75rem 1rem; + box-sizing: border-box; + text-decoration: none; + display: flex; +} +.pagination .left span, +.pagination .right span { + margin: auto; +} + +.page { + display: flex; + flex-direction: column; +} + +.horizontal-button-container { + background-color: white; + box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px; + margin-top: 2rem; + padding: 1rem; + box-sizing: border-box; + flex-direction: column; + border-radius: 5px; +} +.horizontal-button-container button { + height: 2rem; +} + +.search { + margin-top: 2rem; + width: 100%; + height: 3rem; + box-sizing: border-box; +} +.search .title { + font-size: 1.2rem; + font-style: italic; +} +.search .action { + display: flex; + flex-direction: row; +} +.search .action input { + padding: 0.5rem; + box-sizing: border-box; + width: 100%; +} +.search .action button { + margin-left: 1rem; + width: 200px; +} \ No newline at end of file diff --git a/frontend/views/themes/default/css/index.scss b/frontend/views/themes/default/css/index.scss new file mode 100644 index 0000000..16e5d73 --- /dev/null +++ b/frontend/views/themes/default/css/index.scss @@ -0,0 +1,215 @@ +.post-list-container { + margin-top: 2rem; + display: flex; + flex-direction: row; + + .post-list { + width: calc(100% * (2 / 3)); + min-height: 200px; + + .post { + width: 100%; + background-color: #fff; + border-radius: 10px; + box-shadow: #0000001c 0 0px 5px; + min-height: 200px; + padding: 0.5rem 1rem; + box-sizing: border-box; + display: flex; + flex-direction: column; + margin-bottom: 1rem; + + .title { + font-size: 2rem; + font-weight: 550; + margin-bottom: 0.5rem; + text-decoration: none; + color: black; + } + + .authors { + margin-bottom: 1.5rem; + + a { + color: black; + } + } + + .description { + margin-bottom: 1rem; + } + + .badges { + margin-top: auto; + color: #414141; + .tags { + display: flex; + flex-direction: row; + margin-bottom: 0.5rem; + .tag { + } + } + .info { + display: flex; + flex-direction: row; + + .info-blip { + margin-right: 1rem; + } + + .publish-date::before { + background-image: url("../img/calendar.svg"); /* Set the background image */ + } + .reading-time::before { + background-image: url("../img/hourglass.svg"); /* Set the background image */ + } + + .word-count::before { + background-image: url("https://www.svgrepo.com/show/2460/cherry.svg"); /* Set the background image */ + } + } + } + } + } + .post-list.full { + width: 100%; + } + .tag-list { + width: calc(100% * (1 / 3)); + max-width: calc(100% * (1 / 3)); + min-height: 200px; + padding: 0 2rem; + box-sizing: border-box; + + .tag-header { + font-size: 1.2rem; + margin-bottom: 2rem; + } + + .list { + display: flex; + flex-direction: row; + overflow-wrap: break-word; + flex-wrap: wrap; + width: 100%; + .tag { + height: fit-content; + background-color: lightgray; + margin-bottom: 0.5rem; + margin-right: 0.5rem; + } + + .tag::before { + display: none; + } + } + } +} + +.tag { + width: fit-content; + margin-right: 0.1rem; + padding: 0.2rem 0.3rem; + box-sizing: border-box; + border-radius: 4px; + + color: black; + text-decoration: none; +} + +.tag::before { + background-image: url("../img/tag.svg"); /* Set the background image */ +} + +.pagination { + margin-top: auto; + width: 100%; + display: flex; + flex-direction: row; + height: 40px; + + .left { + margin-right: 1rem; + } + .pages { + height: 100%; + margin: auto; + display: flex; + flex-direction: row; + + a { + display: flex; + height: 100%; + aspect-ratio: 1/1; + color: black; + text-decoration: none; + + span { + margin: auto; + } + } + + a.active { + background-color: lightskyblue; + border-radius: 5px; + } + } + .right { + margin-left: 1rem; + } + .left, + .right { + padding: 0.75rem 1rem; + box-sizing: border-box; + text-decoration: none; + display: flex; + span { + margin: auto; + } + } +} + +.page { + display: flex; + flex-direction: column; +} +.horizontal-button-container { + background-color: white; + // min-height: 100px; + box-shadow: #0000001c 0 0px 5px; + margin-top: 2rem; + padding: 1rem; + box-sizing: border-box; + flex-direction: column; + border-radius: 5px; + + button { + height: 2rem; + } +} +.search { + margin-top: 2rem; + width: 100%; + height: 3rem; + box-sizing: border-box; + + .title { + font-size: 1.2rem; + font-style: italic; + } + + .action { + display: flex; + flex-direction: row; + + input { + padding: 0.5rem; + box-sizing: border-box; + width: 100%; + } + button { + margin-left: 1rem; + width: 200px; + } + } +} diff --git a/frontend/views/themes/default/css/login.css b/frontend/views/themes/default/css/login.css new file mode 100644 index 0000000..9dcc99d --- /dev/null +++ b/frontend/views/themes/default/css/login.css @@ -0,0 +1,38 @@ +.page .page-center { + margin-top: 2rem; +} +.page .page-center .page-modal { + width: 400px; + height: 200px; + background-color: white; + margin: auto; + padding: 1rem; + box-sizing: border-box; +} +.page .page-center .page-modal .title { + font-size: 1.4rem; + text-align: center; +} +.page .page-center .page-modal .login-part { + width: 100%; + margin-bottom: 1rem; +} +.page .page-center .page-modal .login-part input { + width: 100%; + padding: 0.25rem; + box-sizing: border-box; +} +.page .page-center .page-modal .action-container { + display: flex; + flex-direction: row-reverse; +} +.page .page-center .page-modal .action-container button { + width: 100px; + padding: 0.25rem; + box-sizing: border-box; + margin-left: auto; + height: 30px; +} +.page .page-center .page-modal .action-container a { + margin: auto auto auto 0; +} \ No newline at end of file diff --git a/frontend/views/themes/default/css/login.scss b/frontend/views/themes/default/css/login.scss new file mode 100644 index 0000000..2814f87 --- /dev/null +++ b/frontend/views/themes/default/css/login.scss @@ -0,0 +1,44 @@ +.page .page-center { + margin-top: 2rem; + + .page-modal { + width: 400px; + height: 200px; + background-color: white; + margin: auto; + padding: 1rem; + box-sizing: border-box; + + .title { + font-size: 1.4rem; + text-align: center; + } + + .login-part { + width: 100%; + margin-bottom: 1rem; + + input { + width: 100%; + padding: 0.25rem; + box-sizing: border-box; + } + } + + .action-container { + display: flex; + flex-direction: row-reverse; + + button { + width: 100px; + padding: 0.25rem; + box-sizing: border-box; + margin-left: auto; + height: 30px; + } + a { + margin: auto auto auto 0; + } + } + } +} diff --git a/frontend/views/themes/default/css/newPost.css b/frontend/views/themes/default/css/newPost.css new file mode 100644 index 0000000..40e8cf4 --- /dev/null +++ b/frontend/views/themes/default/css/newPost.css @@ -0,0 +1,50 @@ +.page .page-center { + background-color: white; + min-height: 100px; + box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px; + margin-top: 2rem; + padding: 1rem; + box-sizing: border-box; + width: 1080px; + max-width: 1080px; + flex-direction: column; + border-radius: 5px; +} +.page .page-center .page-title { + text-align: center; + font-size: 1.5rem; +} +.page .page-center .info-container { + display: flex; + flex-direction: column; + margin-bottom: 1rem; +} +.page .page-center .info-container input { + width: 100%; + padding: 0.5rem; + box-sizing: border-box; + background-color: rgb(245, 245, 245); + border: 1px solid gray; + border-radius: 5px; +} +.page .page-center .info-container textarea { + padding: 0.5rem; + box-sizing: border-box; + max-width: 100%; + min-width: 100%; + min-height: 5rem; + background-color: rgb(245, 245, 245); + border: 1px solid gray; +} +.page .page-center .side-by-side-info { + display: flex; + flex-direction: row; +} +.page .page-center .side-by-side-info input { + margin: 0 0.25rem; +} +.page .page-center .side-by-side-info .button { + padding: 0.5rem; + box-sizing: border-box; + margin: auto; +} \ No newline at end of file diff --git a/frontend/views/themes/default/css/newPost.scss b/frontend/views/themes/default/css/newPost.scss new file mode 100644 index 0000000..542b2f9 --- /dev/null +++ b/frontend/views/themes/default/css/newPost.scss @@ -0,0 +1,60 @@ +.page .page-center { + background-color: white; + min-height: 100px; + box-shadow: #0000001c 0 0px 5px; + margin-top: 2rem; + padding: 1rem; + box-sizing: border-box; + width: 1080px; + max-width: 1080px; + flex-direction: column; + border-radius: 5px; + + .page-title { + text-align: center; + font-size: 1.5rem; + } + + .info-container { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + + .title { + } + + input { + width: 100%; + padding: 0.5rem; + box-sizing: border-box; + background-color: rgb(245, 245, 245); + border: 1px solid gray; + border-radius: 5px; + } + textarea { + padding: 0.5rem; + box-sizing: border-box; + max-width: 100%; + min-width: 100%; + min-height: 5rem; + background-color: rgb(245, 245, 245); + border: 1px solid gray; + // border-radius: 5px; + } + } + + .side-by-side-info { + display: flex; + flex-direction: row; + + input { + margin: 0 0.25rem; + } + + .button { + padding: 0.5rem; + box-sizing: border-box; + margin: auto; + } + } +} diff --git a/frontend/views/themes/default/css/post.css b/frontend/views/themes/default/css/post.css new file mode 100644 index 0000000..8a63dd4 --- /dev/null +++ b/frontend/views/themes/default/css/post.css @@ -0,0 +1,27 @@ +.page .page-center { + 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; + flex-direction: column; + border-radius: 5px; +} +.page .page-center .title { + font-size: 2rem; + font-weight: bold; +} +.page .page-center .image-container { + max-width: 100%; +} +.page .page-center .image-container img { + max-width: 100%; +} +.page .page-center .horizontal-button-container { + width: 100%; +} +.page .page-center .horizontal-button-container button { + height: 2rem; +} \ No newline at end of file diff --git a/frontend/views/themes/default/css/post.scss b/frontend/views/themes/default/css/post.scss new file mode 100644 index 0000000..49e54f2 --- /dev/null +++ b/frontend/views/themes/default/css/post.scss @@ -0,0 +1,33 @@ +.page .page-center { + background-color: white; + // min-height: 100px; + box-shadow: #0000001c 0 0px 5px; + margin-top: 2rem; + padding: 1rem; + box-sizing: border-box; + width: 1080px; + max-width: 1080px; + flex-direction: column; + border-radius: 5px; + + .title { + font-size: 2rem; + font-weight: bold; + } + + .image-container { + max-width: 100%; + + img { + max-width: 100%; + } + } + + .horizontal-button-container { + width: 100%; + + button { + height: 2rem; + } + } +} diff --git a/frontend/views/themes/default/css/settings.css b/frontend/views/themes/default/css/settings.css new file mode 100644 index 0000000..315aecc --- /dev/null +++ b/frontend/views/themes/default/css/settings.css @@ -0,0 +1,106 @@ +.page .page-center { + background-color: white; + width: 700px; + min-height: 50px; + margin-top: 4rem; + padding: 1rem; + box-sizing: border-box; + box-shadow: rgba(0, 0, 0, 0.1098039216) 0 0px 5px; +} +.page .page-center .header { + text-align: center; + font-size: 1.5rem; +} +.page .page-center .setting-list { + display: flex; + flex-direction: column; +} +.page .page-center .setting-list .setting { + width: 100%; + height: 32px; + background-color: rgb(240, 240, 240); + padding: 0.1rem; + box-sizing: border-box; + display: flex; + flex-direction: row; +} +.page .page-center .setting-list .setting .title { + margin: auto auto auto 0; +} +.page .page-center .setting-list .setting .value { + margin: 0 0 0 auto; + display: flex; +} +.page .page-center .setting-list .setting .value input[type=text] { + margin: auto; + font-size: 1rem; + text-align: center; +} +.page .page-center .setting-list .setting .value input[type=number] { + margin: auto; + width: 4rem; + 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); +} + +.switch { + position: relative; + display: inline-block; + width: 50px; + height: 28px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #b8b8b8; + transition: 0.4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 4px; + background-color: white; + transition: 0.4s; +} + +input:checked + .slider { + background-color: #2196f3; +} + +input:focus + .slider { + box-shadow: 0 0 1px #2196f3; +} + +input:checked + .slider:before { + transform: translateX(20px); +} + +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} \ No newline at end of file diff --git a/frontend/views/themes/default/css/settings.scss b/frontend/views/themes/default/css/settings.scss new file mode 100644 index 0000000..0b90ee5 --- /dev/null +++ b/frontend/views/themes/default/css/settings.scss @@ -0,0 +1,110 @@ +.page .page-center { + background-color: white; + width: 700px; + min-height: 50px; + margin-top: 4rem; + padding: 1rem; + box-sizing: border-box; + box-shadow: #0000001c 0 0px 5px; + + .header { + text-align: center; + font-size: 1.5rem; + } + + .setting-list { + display: flex; + flex-direction: column; + + .setting { + width: 100%; + height: 32px; + background-color: rgb(240, 240, 240); + padding: 0.1rem; + box-sizing: border-box; + display: flex; + flex-direction: row; + + .title { + margin: auto auto auto 0; + } + + .value { + margin: 0 0 0 auto; + display: flex; + + input[type="text"] { + margin: auto; + font-size: 1rem; + text-align: center; + } + input[type="number"] { + margin: auto; + width: 4rem; + text-align: right; + font-size: 1rem; + } + } + } + + .setting.fit-column { + flex-direction: column; + width: 100%; + } + + .setting:nth-child(even) { + background-color: rgb(250, 250, 250); + } + } +} + +.switch { + position: relative; + display: inline-block; + width: 50px; + height: 28px; +} +.switch input { + opacity: 0; + width: 0; + height: 0; +} +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #b8b8b8; + -webkit-transition: 0.4s; + transition: 0.4s; +} +.slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: 0.4s; + transition: 0.4s; +} +input:checked + .slider { + background-color: #2196f3; +} +input:focus + .slider { + box-shadow: 0 0 1px #2196f3; +} +input:checked + .slider:before { + -webkit-transform: translateX(20px); + -ms-transform: translateX(20px); + transform: translateX(20px); +} +.slider.round { + border-radius: 34px; +} +.slider.round:before { + border-radius: 50%; +} diff --git a/frontend/views/themes/default/ejs/admin-settings.ejs b/frontend/views/themes/default/ejs/admin-settings.ejs new file mode 100644 index 0000000..d545034 --- /dev/null +++ b/frontend/views/themes/default/ejs/admin-settings.ejs @@ -0,0 +1,72 @@ + + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> +
+
+
<%= website_name %> Admin Settings
+
+
+
User registration
+
+ +
+
+
+
Hide "Login" in navigation bar
+
+ +
+
+
+
Serve ATOM feed
+
+ +
+
+
+
Serve JSON feed
+
+ +
+
+
+
Password minimum length
+
+ +
+
+
+
Website Name
+
+ +
+
+
+
+
+ + <%- include("partials/footer.ejs") %> + + + + diff --git a/frontend/views/themes/default/ejs/author.ejs b/frontend/views/themes/default/ejs/author.ejs new file mode 100644 index 0000000..19a0cac --- /dev/null +++ b/frontend/views/themes/default/ejs/author.ejs @@ -0,0 +1,42 @@ + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> + +
+ <%if(logged_in_user) {%> +
+
+ +
+
+ <%}%> + +
+
+
<%= post.title %>
+ + <%- post.content %> +
+
+
+
<%= post.owner.display_name || post.owner.username %>
+ +
Registered <%= post.created_date.toLocaleString('en-US', { dateStyle:'medium' }) || "Null" %>
+
<%= post.post_count %> Posts
+
+ +
+
+
+
+ + <%- include("partials/footer.ejs") %> + + 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/index.ejs b/frontend/views/themes/default/ejs/index.ejs new file mode 100644 index 0000000..5715dc5 --- /dev/null +++ b/frontend/views/themes/default/ejs/index.ejs @@ -0,0 +1,44 @@ + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> + +
+ <%if(logged_in_user) {%> +
+
+ +
+
+ <%}%> +
+
+
+ <% for(post of blog_list) { %> + + <%- include("partials/post.ejs", {post:post}) %> + + <% } %> +
+
+
TAGS
+
+ <% for(tag of tags) { %> + <%= tag.name %> + <% } %> +
+
+
+ <%- include("partials/pagination.ejs") %> +
+
+ + <%- include("partials/footer.ejs") %> + + diff --git a/frontend/views/themes/default/ejs/login.ejs b/frontend/views/themes/default/ejs/login.ejs new file mode 100644 index 0000000..a98be78 --- /dev/null +++ b/frontend/views/themes/default/ejs/login.ejs @@ -0,0 +1,34 @@ + + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> +
+
+
+
Sign in
+ + +
+ + Register +
+
+
+
+ <%- include("partials/footer.ejs") %> + + + diff --git a/frontend/views/themes/default/ejs/partials/footer.ejs b/frontend/views/themes/default/ejs/partials/footer.ejs new file mode 100644 index 0000000..d77de5a --- /dev/null +++ b/frontend/views/themes/default/ejs/partials/footer.ejs @@ -0,0 +1,11 @@ + diff --git a/frontend/views/themes/default/ejs/partials/header.ejs b/frontend/views/themes/default/ejs/partials/header.ejs new file mode 100644 index 0000000..fbb9070 --- /dev/null +++ b/frontend/views/themes/default/ejs/partials/header.ejs @@ -0,0 +1,30 @@ +
+
+ + + +
+
diff --git a/frontend/views/themes/default/ejs/partials/pagination.ejs b/frontend/views/themes/default/ejs/partials/pagination.ejs new file mode 100644 index 0000000..f109c7b --- /dev/null +++ b/frontend/views/themes/default/ejs/partials/pagination.ejs @@ -0,0 +1,23 @@ + diff --git a/frontend/views/themes/default/ejs/partials/post.ejs b/frontend/views/themes/default/ejs/partials/post.ejs new file mode 100644 index 0000000..23a82e1 --- /dev/null +++ b/frontend/views/themes/default/ejs/partials/post.ejs @@ -0,0 +1,20 @@ +
+ <%= post.title ? post.title : "Untitled Post" %> + +
<%= post.description %>
+
+
+
<%= post.publish_date ? post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) : "Unknown Publish Date" %>
+
Null minute read
+ <% if (logged_in_user) { %> + +
<%= post.visibility %>
+ + <% if (new Date(post.publish_date) > new Date() && post.visibility !== 'PRIVATE') {%> +
Scheduled
+ <% } %> + + <% } %> +
+
+
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 new file mode 100644 index 0000000..1f898a6 --- /dev/null +++ b/frontend/views/themes/default/ejs/post.ejs @@ -0,0 +1,31 @@ + + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> +
+ <%if(logged_in_user) {%> +
+
+ + +
+
+ <%}%> + +
+
<%= blog_post.title %>
+ <%- blog_post.content %> +
+
+ <%- include("partials/footer.ejs") %> + + + + diff --git a/frontend/views/themes/default/ejs/postNew.ejs b/frontend/views/themes/default/ejs/postNew.ejs new file mode 100644 index 0000000..aebec79 --- /dev/null +++ b/frontend/views/themes/default/ejs/postNew.ejs @@ -0,0 +1,51 @@ + + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> +
+
+
New Post
+
+
Title
+ +
+
+
Description
+ +
+
+
Tags
+ +
+
+
+
Content
+ <%- include("partials/richTextEditor.ejs", {text_selector: 'post-content', prefill: existing_blog.raw_content}) %> +
+
+
Post Date
+
+ + +
+
+
+
+ + + +
+
+
+
+ <%- include("partials/footer.ejs") %> + + + diff --git a/frontend/views/themes/default/ejs/postSearch.ejs b/frontend/views/themes/default/ejs/postSearch.ejs new file mode 100644 index 0000000..9e8d863 --- /dev/null +++ b/frontend/views/themes/default/ejs/postSearch.ejs @@ -0,0 +1,37 @@ + + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> + +
+
+ +
+
+ <% for(post of blog_list) { %> + + <%- include("partials/post.ejs", {post:post}) %> + + <% } %> +
+
+ <%- include("partials/pagination.ejs") %> +
+
+ + <%- include("partials/footer.ejs") %> + + + diff --git a/frontend/views/themes/default/ejs/register.ejs b/frontend/views/themes/default/ejs/register.ejs new file mode 100644 index 0000000..cc1b408 --- /dev/null +++ b/frontend/views/themes/default/ejs/register.ejs @@ -0,0 +1,34 @@ + + + + + + + + Yet-Another-Blog + + + <%- include("partials/header.ejs") %> +
+
+
+
Register
+ + +
+ + Login +
+
+
+
+ <%- include("partials/footer.ejs") %> + + + diff --git a/frontend/views/themes/default/img/calendar.svg b/frontend/views/themes/default/img/calendar.svg new file mode 100644 index 0000000..5d9e723 --- /dev/null +++ b/frontend/views/themes/default/img/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/views/themes/default/img/hourglass.svg b/frontend/views/themes/default/img/hourglass.svg new file mode 100644 index 0000000..5b6554f --- /dev/null +++ b/frontend/views/themes/default/img/hourglass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/views/themes/default/img/json.svg b/frontend/views/themes/default/img/json.svg new file mode 100644 index 0000000..8ef5c55 --- /dev/null +++ b/frontend/views/themes/default/img/json.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/views/themes/default/img/news.svg b/frontend/views/themes/default/img/news.svg new file mode 100644 index 0000000..520f031 --- /dev/null +++ b/frontend/views/themes/default/img/news.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/views/themes/default/img/rss.svg b/frontend/views/themes/default/img/rss.svg new file mode 100644 index 0000000..baeeb11 --- /dev/null +++ b/frontend/views/themes/default/img/rss.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/views/themes/default/img/tag.svg b/frontend/views/themes/default/img/tag.svg new file mode 100644 index 0000000..967efed --- /dev/null +++ b/frontend/views/themes/default/img/tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/img/textarea/sidebyside.svg b/frontend/views/themes/default/img/textarea/sidebyside.svg similarity index 87% rename from frontend/public/img/textarea/sidebyside.svg rename to frontend/views/themes/default/img/textarea/sidebyside.svg index 406a71e..c473cb1 100644 --- a/frontend/public/img/textarea/sidebyside.svg +++ b/frontend/views/themes/default/img/textarea/sidebyside.svg @@ -6,7 +6,7 @@ version="1.1" id="svg1" sodipodi:docname="sidebyside.svg" - inkscape:version="1.3.1 (91b66b0783, 2023-11-16, custom)" + inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -23,10 +23,10 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:zoom="24.291667" - inkscape:cx="20.006861" + inkscape:cx="20.027444" inkscape:cy="16.013722" inkscape:window-width="2560" - inkscape:window-height="1370" + inkscape:window-height="1368" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -34,5 +34,5 @@ + style="fill:#000000" /> diff --git a/frontend/public/img/textarea/video.svg b/frontend/views/themes/default/img/textarea/video.svg similarity index 88% rename from frontend/public/img/textarea/video.svg rename to frontend/views/themes/default/img/textarea/video.svg index 11aec2c..b6d234e 100644 --- a/frontend/public/img/textarea/video.svg +++ b/frontend/views/themes/default/img/textarea/video.svg @@ -6,7 +6,7 @@ version="1.1" id="svg1" sodipodi:docname="video.svg" - inkscape:version="1.3.1 (91b66b0783, 2023-11-16, custom)" + inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -23,10 +23,10 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050" inkscape:zoom="24.291667" - inkscape:cx="20.006861" - inkscape:cy="20.006861" + inkscape:cx="20.027444" + inkscape:cy="20.048027" inkscape:window-width="2560" - inkscape:window-height="1370" + inkscape:window-height="1368" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -34,5 +34,5 @@ + style="fill:#000000" /> diff --git a/frontend/views/themes/default/js/admin.js b/frontend/views/themes/default/js/admin.js new file mode 100644 index 0000000..8acbd17 --- /dev/null +++ b/frontend/views/themes/default/js/admin.js @@ -0,0 +1,24 @@ +async function toggleState(setting_name, element) { + console.log(element.checked); + const form = { + setting_name: setting_name, + value: element.checked, + }; + const response = await request("/setting", "POST", form); + + // TODO: On failure, notify the user + if (response.body.success) { + } +} + +async function changeValue(setting_name, element) { + const form = { + setting_name: setting_name, + value: element.value, + }; + const response = await request("/setting", "POST", form); + + // TODO: On failure, notify the user + if (response.body.success) { + } +} 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/public/js/generic.js b/frontend/views/themes/default/js/generic.js similarity index 100% rename from frontend/public/js/generic.js rename to frontend/views/themes/default/js/generic.js diff --git a/frontend/public/js/register.js b/frontend/views/themes/default/js/login.js similarity index 50% rename from frontend/public/js/register.js rename to frontend/views/themes/default/js/login.js index ac27160..8c42b6a 100644 --- a/frontend/public/js/register.js +++ b/frontend/views/themes/default/js/login.js @@ -1,3 +1,21 @@ +async function requestLogin() { + const account_information = { + username: qs("#username").value, + password: qs("#password").value, + }; + + const account_response = await request("/login", "POST", account_information); + + // Check response for errors + + // If success, return to account + console.log(account_response); + + if (account_response.body.success) { + location.href = "/"; + } +} + async function requestRegister() { const account_information = { username: qs("#username").value, diff --git a/frontend/views/themes/default/js/newPost.js b/frontend/views/themes/default/js/newPost.js new file mode 100644 index 0000000..b7e9bd3 --- /dev/null +++ b/frontend/views/themes/default/js/newPost.js @@ -0,0 +1,27 @@ +let blog_id = window.location.href.split("/")[4]; + +async function publish(visibility) { + let form_data = { + title: qs("#post-title").value, + description: qs("#post-description").value, + tags: [], + media: media, + visibility: visibility, + content: qs("#post-content").value, + date: qs("#date").value, + time: qs("#time").value, + id: blog_id, + }; + + // Get our tags, trim them, then shove them into an array + const tags_value = qs("#post-tags").value || ""; + if (tags_value.length) { + let tags_array = qs("#post-tags").value.split(","); + tags_array.forEach((tag) => form_data.tags.push(tag.trim())); + } + const post_response = await request("/api/web/post", "PATCH", form_data); + + if (post_response.body.success) { + window.location.href = `/post/${post_response.body.post_id}`; + } +} diff --git a/frontend/views/themes/default/js/post.js b/frontend/views/themes/default/js/post.js new file mode 100644 index 0000000..0250e23 --- /dev/null +++ b/frontend/views/themes/default/js/post.js @@ -0,0 +1,6 @@ +let post_id = window.location.href.split("/")[4]; + +async function deletePost() { + const post_response = await request("/api/web/post", "DELETE", { id: post_id }); +} +function editPost() {} diff --git a/frontend/public/js/postList.js b/frontend/views/themes/default/js/postSearch.js similarity index 75% rename from frontend/public/js/postList.js rename to frontend/views/themes/default/js/postSearch.js index 8de545a..1710603 100644 --- a/frontend/public/js/postList.js +++ b/frontend/views/themes/default/js/postSearch.js @@ -1,5 +1,3 @@ -qs("#search-btn").addEventListener("click", search); - function search() { const url_query = `search=${qs("input").value}`; window.location.href = `${window.location.origin}${window.location.pathname}?${url_query}`; diff --git a/frontend/views/themes/default/js/richTextEditor.js b/frontend/views/themes/default/js/richTextEditor.js new file mode 100644 index 0000000..06611bb --- /dev/null +++ b/frontend/views/themes/default/js/richTextEditor.js @@ -0,0 +1,101 @@ +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), + content_type: image_object.content_type, + post_id: window.location.href.split("/")[4], + parent_type: "posts", + }; + + 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/frontend/views/themes/default/manifest.json b/frontend/views/themes/default/manifest.json new file mode 100644 index 0000000..696fef9 --- /dev/null +++ b/frontend/views/themes/default/manifest.json @@ -0,0 +1,14 @@ +{ + "author": "Armored Dragon", + "version": "1.0.0", + "comment": "The default theme for Yet-Another-Blog", + "pages": { + "index": "/ejs/index.ejs", + "login": "/ejs/login.ejs", + "register": "/ejs/register.ejs", + "author": "/ejs/author.ejs", + "post": "/ejs/post.ejs", + "settings": "/ejs/login.ejs", + "user-settings": "/ejs/user-settings.ejs" + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 55420fd..24db92b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,45 +11,45 @@ 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[] - visibility PostStatus @default(UNLISTED) + media String[] + visibility PostStatus @default(DRAFT) owner User? @relation(fields: [ownerid], references: [id], onDelete: Cascade) ownerid String? // Tags - tags String[] + tags Tag[] // Dates - publish_date DateTime? + publish_date DateTime? @default(now()) created_date DateTime @default(now()) } 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 { @@ -62,6 +62,18 @@ model Group { permissions String[] } +model Tag { + id String @id @unique @default(uuid()) + name String @unique + type TagMode @default(NORMAL) + posts Post[] +} + +enum TagMode { + NORMAL + ALIAS +} + enum Role { LOCKED USER @@ -69,6 +81,8 @@ enum Role { } enum PostStatus { + DRAFT + PRIVATE UNLISTED PUBLISHED } diff --git a/yab.js b/yab.js index ccbc1fd..296e3dc 100644 --- a/yab.js +++ b/yab.js @@ -12,10 +12,13 @@ const internal = require("./backend/core/internal_api"); // Express settings app.set("view-engine", "ejs"); app.set("views", path.join(__dirname, "frontend/views")); -app.use(express.static(path.join(__dirname, "frontend/public"))); app.use(express.json({ limit: "500mb" })); app.use(express.urlencoded({ extended: false })); +// TODO: Does this persist previous themes? May cause security issues! +const refreshTheme = (theme_name) => app.use(express.static(path.join(__dirname, `frontend/views/themes/${theme_name}`))); +refreshTheme("default"); + app.use( session({ secret: require("crypto").randomBytes(128).toString("base64"), @@ -28,10 +31,12 @@ app.use( app.post("/login", checkNotAuthenticated, internal.postLogin); app.post("/register", checkNotAuthenticated, internal.postRegister); app.post("/setting", checkAuthenticated, internal.postSetting); -app.post("/api/web/blog", checkAuthenticated, internal.postBlog); -app.delete("/api/web/blog/image", checkAuthenticated, internal.deleteImage); -app.delete("/api/web/blog", checkAuthenticated, internal.deleteBlog); -app.patch("/api/web/blog", checkAuthenticated, internal.patchBlog); +app.post("/api/web/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); @@ -40,11 +45,12 @@ 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("/blog", page_scripts.blogList); -app.get("/blog/new", checkAuthenticated, page_scripts.blogNew); -app.get("/blog/:blog_id", page_scripts.blogSingle); -app.get("/blog/:blog_id/edit", checkAuthenticated, page_scripts.blogEdit); +app.get("/posts", page_scripts.blogList); +app.get("/post/new", checkAuthenticated, page_scripts.blogNew); +app.get("/post/:blog_id", page_scripts.blogSingle); +app.get("/post/:blog_id/edit", checkAuthenticated, page_scripts.blogEdit); app.get("/atom", page_scripts.atom); app.get("/json", page_scripts.jsonFeed);