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/admin-setting-toggle.ejs", {setting: {name: 'CD_RSS', name_pretty: 'RSS Feed', enabled: settings.CD_RSS}}) %>
-
- <%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'CD_AP', name_pretty: 'Activity Pub Feed', enabled: settings.CD_AP}}) %>
-
-
-
-
TODO
-
-
- <%- include("partials/admin-setting-text.ejs", {setting: {name: 'WEBSITE_NAME', name_pretty: 'Website Title', value: settings.WEBSITE_NAME}}) %>
-
-
- <%- include("partials/admin-setting-text.ejs", {setting: {name: 'PLAUSIBLE_URL', name_pretty: 'Plausible URL', value: settings.PLAUSIBLE_URL}}) %>
-
-
-
-
- <%- 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.raw_images?.length) { %> <% for (image in existing_blog.raw_images) {%>
-
-
-
-
- <%}%> <% } else {%>
-
Drop images here
- <% } %>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <% 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'}) %>
-
-
-
- <%- 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 @@
-
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 @@
-
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'}) %>
-
-
-
- <%- 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") %>
+
+
+
+
+
+
User registration
+
+
+
+
+
+
Hide "Login" in navigation bar
+
+
+
+
+
+
Serve ATOM feed
+
+
+
+
+
+
Serve JSON feed
+
+
+
+
+
+
Password minimum length
+
+
+
+
+
+
+
+
+
+ <%- 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") %>
+
+
+
+
+
+
+
Change Password
+
+
+
+
Change Profile Picture
+
+
+
+
+
+
+
+
+ <%- 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}) %>
+
+ <% } %>
+
+
+
+ <%- 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") %>
+
+ <%- 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
+
+
+
+
+
+
Content
+ <%- include("partials/richTextEditor.ejs", {text_selector: 'post-content', prefill: existing_blog.raw_content}) %>
+
+
+
+
+
+
+
+
+
+
+
+ <%- 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") %>
+
+ <%- 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);