Generic Theme (#1)

* Theme work

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* User registration.
Cleanup CSS.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Post Creation and Manipulation
Uploading images now easier. Just drag and drop onto the text area.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Author Page.
Edit author page.
Author display name.
Generic media uploads.
Core refactoring.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Texteditor bugfix.
PGAdmin docker container for management of database.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Tags.
Search by tags.
Return tags used by posts.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* New post button.
Fix index "page" param not being honored.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Post drafts
Users can now only have one "unpublished" draft.
Improved password handling.
Minor cleanup.
Admin panel navigation link.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Post visibility flairs

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Publish date autofill to now.
Fix deleteBlog.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Removed unused function

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Media upload pruning.
Uploaded media is now pruned automatically every time a post is updated.
Minor cleanup.
Groundwork for media types other than images.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

* Updated name.
Use the manifest data.

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>

---------

Signed-off-by: Armored Dragon <publicmail@armoreddragon.com>
pull/2/head
Armored Dragon 2024-04-30 10:26:35 -05:00 committed by GitHub
parent 50f30f227d
commit fc83b5bbe9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 2799 additions and 3001 deletions

View File

@ -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 })) || [];
// Add new tags
for (let tag_index = 0; post_content.tags.length > tag_index; tag_index++) {
let tag = post_content.tags[tag_index];
// Check to see if tag exists, create if necessary,
let database_tag = await prisma.tag.upsert({ where: { name: tag }, update: {}, create: { name: tag } });
database_tag_list.push(database_tag);
}
// 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],
};
await prisma.blogPost.update({ where: { id: post.data.id }, data: blog_post_formatted });
// Save the updated post to the database
await prisma.post.update({ where: { id: post.id }, data: post_formatted });
let uploaded_images = [];
let uploaded_thumbnail = "DEFAULT";
// Prune the post to save on storage
await pruneMedia({ parent_id: post_id, parent_type: "posts" });
// 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);
}
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 });
let data_to_update = {
images: [...post.data.raw_images, ...uploaded_images],
};
if (!user.success) return { success: false, message: user.message || "User does not exist" };
user = user.data;
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;
if (!post.success) return { success: false, message: post.message || "Post does not exist" };
post = post.data;
data_to_update.thumbnail = uploaded_thumbnail;
}
let can_delete = post.owner.id === user.id || user.role === "ADMIN";
await prisma.blogPost.update({ where: { id: post.data.id }, data: data_to_update });
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,37 +475,30 @@ 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) {
function _formatBlogContent(content, media_list) {
// Replace Images
const image_regex = /{image:([^}]+)}/g;
@ -431,10 +512,11 @@ function _format_blog_content(content, images) {
return `<div class='video-embed'><iframe src="${_getVideoEmbed(inner_content)}" frameborder="0" allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
});
// Replace Images
content = content.replace(image_regex, (match, image_name) => {
for (image of images) {
if (image.includes(image_name)) {
return `<div class='image-container'><img src='${image}'></div>`;
for (media of media_list) {
if (media.includes(image_name)) {
return `<div class='image-container'><img src='${media}'></div>`;
}
}
@ -467,13 +549,8 @@ function _format_blog_content(content, images) {
}
}
}
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 } });
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;
}
module.exports = { settings, registerUser, getUser, getAuthorPage, postBlog, updateBlog, getBlog, deleteBlog, deleteImage, postSetting, getSetting };
return _r(true);
}
function _stripPrivatePost(post) {
if (!post) return;
if (post.owner) delete post.owner.password;
return post;
}
const _r = (s, m) => {
return { success: s, message: m };
};
module.exports = { settings, newUser, getUser, editUser, getPost, newPost, editPost, deletePost, getBiography, updateBiography, uploadMedia, getTags, postSetting, getSetting };

View File

@ -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) => {

View File

@ -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 };

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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%;
}
}

View File

@ -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%;
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}
}

View File

@ -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%;
}
}

View File

@ -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%;
}
}

View File

@ -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
}

View File

@ -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();
});
}

View File

@ -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 = "/";
}
}

View File

@ -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) => `<div class="image"><img data-image_id="${img_id}" src="${img_url}" /><div><a onclick="deleteImage('${img_id}')">X</a></div></div>`;
// 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();

View File

@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="/css/admin.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | Administration</title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<div class="container">
<div class="category-navigation">
<button id="accounts-nav-btn" class="active" onclick="toggleActiveCategory('accounts')"><span>Accounts</span></button>
<button id="content-delivery-nav-btn" onclick="toggleActiveCategory('content-delivery')"><span>Content Delivery</span></button>
<button id="groups-nav-btn" onclick="toggleActiveCategory('groups')"><span>Groups</span></button>
<button id="yab-master-nav-btn" onclick="toggleActiveCategory('yab-master')"><span>YAB</span></button>
</div>
<div id="accounts" class="category-page">
<!-- Account registration -->
<%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'ACCOUNT_REGISTRATION', name_pretty: 'Account registration', enabled:
settings.ACCOUNT_REGISTRATION}}) %>
<!-- Hide login -->
<%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'HIDE_LOGIN', name_pretty: 'Hide Login', enabled: settings.HIDE_LOGIN}}) %>
<!-- Minimum password length -->
<%- include("partials/admin-setting-number.ejs", {setting: {name:'USER_MINIMUM_PASSWORD_LENGTH', name_pretty: 'Minimum Password Length', value:
settings.USER_MINIMUM_PASSWORD_LENGTH}}) %>
</div>
<div id="content-delivery" class="category-page hidden">
<!-- RSS -->
<%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'CD_RSS', name_pretty: 'RSS Feed', enabled: settings.CD_RSS}}) %>
<!-- ActivityPub -->
<%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'CD_AP', name_pretty: 'Activity Pub Feed', enabled: settings.CD_AP}}) %>
<!-- TODO: Add these option automatically to footer on active -->
</div>
<div id="groups" class="category-page hidden">TODO</div>
<div id="yab-master" class="category-page hidden">
<!-- Website title -->
<%- include("partials/admin-setting-text.ejs", {setting: {name: 'WEBSITE_NAME', name_pretty: 'Website Title', value: settings.WEBSITE_NAME}}) %>
<!-- Placeholder thumbnail -->
<!-- Plausible analytics -->
<%- include("partials/admin-setting-text.ejs", {setting: {name: 'PLAUSIBLE_URL', name_pretty: 'Plausible URL', value: settings.PLAUSIBLE_URL}}) %>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/admin.js"></script>

View File

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="/css/blogSingle.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | <%= blog_post.title %></title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<%if(logged_in_user) {%>
<div class="blog-admin">
<div class="horizontal-button-container">
<a class="yellow" href=""><span>Edit Profile</span></a>
</div>
</div>
<%}%>
<div class="title"><%= blog_post.title%></div>
<%- blog_post.content %>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/blogSingle.js"></script>

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="/css/index.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<link rel="stylesheet" type="text/css" href="/css/blog-list.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | Home</title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<%if(logged_in_user) {%> <%- include("partials/blog-admin.ejs") %> <%}%>
<!-- Search area -->
<div class="search-area">
<input type="text" placeholder="Search..." /><button id="search-btn"><span>Search</span></button>
</div>
<!-- -->
<% for(post of blog_list) { %>
<!-- -->
<%- include("partials/blog-entry.ejs", {post:post}) %>
<!-- -->
<% } %> <%- include("partials/pagination.ejs") %>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/postList.js"></script>

View File

@ -1,105 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="/css/blogNew.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | New Blog</title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<div class="e-header">
<div class="e-thumbnail">
<%if(existing_blog?.thumbnail) {%>
<img src="<%= existing_blog?.thumbnail %>" />
<%} else {%>
<img src="/img/.dev/square.png" />
<% } %>
</div>
<div class="e-description">
<input id="title" type="text" placeholder="Title..." value="<%= existing_blog.title %>" />
<textarea id="description" placeholder="Description..."><%= existing_blog.description %></textarea>
</div>
</div>
<div class="e-image-area">
<% if(existing_blog.raw_images?.length) { %> <% for (image in existing_blog.raw_images) {%>
<div class="image">
<img data-image_id="<%=existing_blog.raw_images[image]%>" src="<%= existing_blog.images[image] %>" />
<div><a onclick="deleteImage('<%= existing_blog.raw_images[image] %>')">X</a></div>
</div>
<%}%> <% } else {%>
<div class="placeholder">Drop images here</div>
<% } %>
</div>
<div class="e-content">
<div class="text-actions">
<div class="left">
<a id="insert-h1"><span>H1</span></a>
<a id="insert-h2"><span>H2</span></a>
<a id="insert-h3"><span>H3</span></a>
<a id="insert-h4"><span>H4</span></a>
<a id="insert-underline">
<span><u>U</u></span>
</a>
<a id="insert-italics">
<span><i>i</i></span>
</a>
<a id="insert-bold">
<span><strong>B</strong></span>
</a>
<a id="insert-strike">
<span><del>S</del></span>
</a>
<a id="insert-sup">
<span><sup>Sup</sup></span>
</a>
</div>
<div class="right">
<a id="insert-sidebyside"><img src="/img/textarea/sidebyside.svg" /></a>
<a id="insert-video"><img src="/img/textarea/video.svg" /></a>
</div>
</div>
<textarea id="content" placeholder="Tell us about your subject..."><%= existing_blog.raw_content %></textarea>
</div>
<div class="e-tags">
<input id="tags" type="text" placeholder="Enter a comma separated list of tags" value="<%= existing_blog.tags %>" />
</div>
<div class="e-settings">
<div class="publish-date">
<div>Publish On</div>
<% if(existing_blog.publish_date) {%>
<input id="date" type="date" value="<%= existing_blog.publish_date %>" />
<%} else { %>
<input id="date" type="date" value="1990-01-01" />
<% } %>
<!-- -->
<% if(existing_blog.publish_date) {%>
<input id="time" type="time" value="<%= existing_blog.publish_time %>" />
<%} else { %>
<input id="time" type="time" value="13:00" />
<% } %>
</div>
</div>
<div class="e-settings">
<div class="horizontal-buttons">
<button onclick="publishBlog(true)" class="yellow"><span>Unlisted</span></button>
<% if(existing_blog.id){%>
<button onclick="publishBlog(false, true)"><span>Edit</span></button>
<% } else {%>
<button onclick="publishBlog()"><span>Publish</span></button>
<% } %>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/newBlog.js"></script>

View File

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="/css/blogSingle.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | <%= blog_post.title %></title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<!-- TODO: Check if user is the owner or an admin -->
<!-- TODO: Confirmation before deleting post -->
<%if(logged_in_user) {%>
<div class="blog-admin">
<div class="horizontal-button-container">
<a id="delete-post" href="#" class="bad"><span>Delete Post</span></a>
<a class="yellow" href="/blog/<%=blog_post.id %>/edit"><span>Edit Post</span></a>
</div>
</div>
<%}%>
<div class="title"><%= blog_post.title%></div>
<%- blog_post.content %>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/blogSingle.js"></script>

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="/css/index.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<link rel="stylesheet" type="text/css" href="/css/blog-list.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | Home</title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<%- include("partials/blog-entry.ejs", {thumbnail: '/img/.dev/square.png', title: 'Title', description: 'Description', author: 'Author'}) %>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/login.js"></script>

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="/css/signin.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | Login</title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<div class="center-modal">
<div class="modal-title">Login</div>
<div class="input-line">
<div>Username</div>
<input id="username" type="text" />
</div>
<div class="input-line">
<div>Password</div>
<input id="password" type="password" />
</div>
<div class="horizontal-button-container">
<button onclick="requestLogin()"><span>Login</span></button>
<a href="/register"><span>Register</span></a>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/login.js"></script>

View File

@ -1,15 +0,0 @@
<div class="setting-row">
<div class="setting-title"><%=setting.name_pretty%></div>
<div class="setting-toggleable">
<div class="spinner hidden">
<div>↺</div>
</div>
<input
type="number"
id="<%=setting.name%>"
onkeydown="updateValue(event.keyCode, '<%=setting.name%>', this.value, this.id)"
placeholder="<%=setting.name_pretty%>"
value="<%=setting.value%>"
/>
</div>
</div>

View File

@ -1,15 +0,0 @@
<div class="setting-row">
<div class="setting-title"><%=setting.name_pretty%></div>
<div class="setting-toggleable">
<div class="spinner hidden">
<div>↺</div>
</div>
<input
id="<%=setting.name%>"
type="text"
onkeydown="updateValue(event.keyCode, '<%=setting.name%>', this.value, this.id)"
placeholder="<%=setting.name_pretty%>"
value="<%=setting.value%>"
/>
</div>
</div>

View File

@ -1,13 +0,0 @@
<div class="setting-row">
<div class="setting-title"><%=setting.name_pretty%></div>
<div class="setting-toggleable">
<div class="spinner hidden">
<div>↺</div>
</div>
<% if (!setting.enabled) { %>
<button id="<%=setting.name%>_toggle" onclick="toggleState('<%=setting.name%>', true, this.id)" class="bad"><div>Disabled</div></button>
<% } else { %>
<button id="<%=setting.name%>_toggle" onclick="toggleState('<%=setting.name%>', false, this.id)" class="good"><div>Enabled</div></button>
<%}%>
</div>
</div>

View File

@ -1,5 +0,0 @@
<div class="blog-admin">
<div class="horizontal-button-container">
<a href="/blog/new"><span>New Post</span></a>
</div>
</div>

View File

@ -1,16 +0,0 @@
<div class="blog-entry">
<a href="/blog/<%= post.id %>" class="thumbnail">
<img src="<%= post.thumbnail %>" />
</a>
<div class="blog-info">
<div class="blog-title">
<a href="/blog/<%= post.id %>"><%= post.title %> </a>
<a href="/author/<%= post.owner.id %>" class="author">By: <%= post.owner.username %></a>
</div>
<div class="blog-description"><%= post.description %></div>
<div class="blog-action">
<div class="date"><%= post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) %></div>
<a href="/blog/<%= post.id %>">Read this post -></a>
</div>
</div>
</div>

View File

@ -1,21 +0,0 @@
<div class="header">
<div class="left">
<a class="nav-button" href="/">
<div><%= website_name %></div>
</a>
</div>
<div class="right">
<a class="nav-button" href="/blog">
<div>Blog</div>
</a>
<% if (logged_in_user) { %>
<a class="nav-button" href="/author/<%= logged_in_user.username %>">
<div>Profile</div>
</a>
<% } else {%> <% if(!settings.HIDE_LOGIN) {%>
<a class="nav-button" href="/login">
<div>Login</div>
</a>
<% } %> <% } %>
</div>
</div>

View File

@ -1,29 +0,0 @@
<div class="pagination">
<% if(pagination.includes(Number(current_page) - 1)) {%>
<a href="<%= loaded_page %>?page=<%= Number(current_page) - 1 %>">
<span>Previous</span>
</a>
<% } else {%>
<a class="disabled">
<span>Previous</span>
</a>
<%}%>
<div class="page-list">
<% for(page of pagination) { %> <% if (page == current_page) {%>
<a href="#" class="active"><span><%=page + 1%></span></a>
<% } else { %>
<a href="<%= loaded_page %>?page=<%=page %>" class=""><span><%=page + 1%></span></a>
<% } %> <% } %>
</div>
<% if(pagination.includes(Number(current_page) + 1)) {%>
<a href="<%= loaded_page %>?page=<%= Number(current_page) + 1 %>">
<span>Next</span>
</a>
<% } else {%>
<a class="disabled">
<span>Next</span>
</a>
<%}%>
</div>

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="/css/signin.css" />
<link rel="stylesheet" type="text/css" href="/css/theme.css" />
<script src="/js/generic.js"></script>
<title><%= website_name %> | Register</title>
</head>
<body>
<%- include("partials/header.ejs", {selected: 'home'}) %>
<div class="page">
<div class="center-modal">
<div class="modal-title">Register</div>
<div class="input-line">
<div>Username</div>
<input id="username" type="text" />
</div>
<div class="input-line">
<div>Password</div>
<input id="password" type="password" />
</div>
<div class="horizontal-button-container">
<button onclick="requestRegister()"><span>Register</span></button>
<a href="/login"><span>Login</span></a>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/register.js"></script>

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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%;
}

View File

@ -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%;
}

View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="../css/settings.css" />
<link rel="stylesheet" type="text/css" href="../css/generic.css" />
<script src="/js/generic.js"></script>
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<div class="page-center">
<div class="header"><%= website_name %> Admin Settings</div>
<div class="setting-list">
<div class="setting">
<div class="title">User registration</div>
<div class="value">
<label class="switch">
<input <% if(settings.ACCOUNT_REGISTRATION) {%> checked <% } %> id="ACCOUNT_REGISTRATION" onchange="toggleState(this.id, this)" type="checkbox" />
<span class="slider round"></span>
</label>
</div>
</div>
<div class="setting">
<div class="title">Hide "Login" in navigation bar</div>
<div class="value">
<label class="switch">
<input <% if(settings.HIDE_LOGIN) {%> checked <% } %> id="HIDE_LOGIN" onchange="toggleState(this.id, this)" type="checkbox" />
<span class="slider round"></span>
</label>
</div>
</div>
<div class="setting">
<div class="title">Serve ATOM feed</div>
<div class="value">
<label class="switch">
<input <% if(settings.CD_RSS) {%> checked <% } %> id="CD_RSS" onchange="toggleState(this.id, this)" type="checkbox" />
<span class="slider round"></span>
</label>
</div>
</div>
<div class="setting">
<div class="title">Serve JSON feed</div>
<div class="value">
<label class="switch">
<input <% if(settings.CD_JSON) {%> checked <% } %> id="CD_JSON" onchange="toggleState(this.id, this)" type="checkbox" />
<span class="slider round"></span>
</label>
</div>
</div>
<div class="setting">
<div class="title">Password minimum length</div>
<div class="value">
<input id="USER_MINIMUM_PASSWORD_LENGTH" value="<%- settings.USER_MINIMUM_PASSWORD_LENGTH -%>" onchange="changeValue(this.id, this)" type="number" />
</div>
</div>
<div class="setting">
<div class="title">Website Name</div>
<div class="value">
<input id="WEBSITE_NAME" value="<%- settings.WEBSITE_NAME -%>" onchange="changeValue(this.id, this)" type="text" />
</div>
</div>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/admin.js"></script>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="../css/author.css" />
<link rel="stylesheet" type="text/css" href="../css/generic.css" />
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<%if(logged_in_user) {%>
<div class="page-center">
<div class="horizontal-button-container">
<button onclick="location='/author/<%= post.owner.id %>/edit'" class="button"><span>Edit Account</span></button>
</div>
</div>
<%}%>
<div class="page-center nobackground">
<div class="biography">
<div class="title"><%= post.title %></div>
<%- post.content %>
</div>
<div class="about">
<div class="profile-picture"><img src="<%= post.profile_picture %>" /></div>
<div class="displayname"><%= post.owner.display_name || post.owner.username %></div>
<!-- TODO: Format Date/time -->
<div class="stat">Registered <%= post.created_date.toLocaleString('en-US', { dateStyle:'medium' }) || "Null" %></div>
<div class="stat"><%= post.post_count %> Posts</div>
<div class="sociallist">
<!-- <a class="link" href="#"></a> -->
</div>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/css/settings.css" />
<link rel="stylesheet" type="text/css" href="/css/generic.css" />
<script src="/js/generic.js"></script>
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<div class="page-center">
<div class="header">User Settings</div>
<div class="setting-list">
<div class="setting">
<div class="title">Display Name</div>
<div class="value">
<input id="display_name" value="<%- profile.owner.display_name -%>" onchange="changeValue(this.id, this)" type="text" />
</div>
</div>
<div class="setting">
<div class="title">Change Password</div>
<button class="button bad"><span>Open Dialog</span></button>
</div>
<div class="setting">
<div class="title">Change Profile Picture</div>
<div class="value">
<input id="profile_picture" onchange="changeValue(this.id, this)" type="file" />
</div>
</div>
</div>
</div>
<div class="page-center">
<div class="header">Biography</div>
<%- include("partials/richTextEditor.ejs", {text_selector: 'post-content', prefill: profile.raw_content}) %>
<button class="button" onclick="updateBiography()"><span>Save Biography</span></button>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/editAuthor.js"></script>

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/css/index.css" />
<link rel="stylesheet" type="text/css" href="/css/generic.css" />
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<%if(logged_in_user) {%>
<div class="page-center">
<div class="horizontal-button-container">
<button onclick="window.location = '/post/new'" class="button"><span>New Post</span></button>
</div>
</div>
<%}%>
<div class="page-center">
<div class="post-list-container">
<div class="post-list">
<% for(post of blog_list) { %>
<!-- -->
<%- include("partials/post.ejs", {post:post}) %>
<!-- -->
<% } %>
</div>
<div class="tag-list">
<div class="tag-header">TAGS</div>
<div class="list">
<% for(tag of tags) { %>
<a class="tag icon" href="/posts/?search=<%= tag.name %> "><%= tag.name %></a>
<% } %>
</div>
</div>
</div>
<%- include("partials/pagination.ejs") %>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/css/login.css" />
<link rel="stylesheet" type="text/css" href="/css/generic.css" />
<script src="/js/generic.js"></script>
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<div class="page-center">
<div class="page-modal">
<div class="title">Sign in</div>
<div class="login-part">
<div class="name">Username:</div>
<input id="username" type="text" />
</div>
<div class="login-part">
<div class="name">Password:</div>
<input id="password" type="password" />
</div>
<div class="action-container">
<button onclick="requestLogin()" class="button"><span>Login</span></button>
<a href="/register">Register</a>
</div>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/login.js"></script>

View File

@ -0,0 +1,11 @@
<div class="footer">
<div class="page-center">
<div class="resources">
<a class="atom-feed icon" href="#"><span>ATOM Feed</span> </a>
<a class="json icon" href="#"><span>JSON Feed</span></a>
</div>
<div class="info">
<a class="json icon" href="https://github.com/armored-dragon/yet-another-blog">Built using Yet-Another-Blog</a>
</div>
</div>
</div>

View File

@ -0,0 +1,30 @@
<div class="header">
<div class="page-center">
<a class="logo" href="/">
<span class="logo-icon icon"></span>
<span class="logo-title"><%= website_name %></span>
</a>
<div class="navigation">
<a href="/posts">
<span>Posts</span>
</a>
<!-- -->
<% if (logged_in_user) { %>
<a href="/author/<%= logged_in_user.id %>">
<span>Account</span>
</a>
<% } else {%> <% if(!settings.HIDE_LOGIN) {%>
<a href="/login">
<span>Login</span>
</a>
<% } %> <% } %>
<!-- -->
<% if (logged_in_user?.role == 'ADMIN') { %>
<a href="/admin">
<span>Admin</span>
</a>
<% } %>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
<div class="pagination">
<% if(pagination.includes(Number(current_page) - 1)) {%>
<a href="<%= loaded_page %>?page=<%= Number(current_page) - 1 %>">
<a href="<%= loaded_page %>?page=<%= Number(current_page) - 1 %>" class="left button"><span>< Previous</span></a>
</a>
<% } else {%>
<button href="#" class="left button disabled"><span>< Previous</span></button>
<%}%>
<!-- -->
<div class="pages">
<% for(page of pagination) { %> <% if (page == current_page) {%>
<a class="active" href="#"><span><%=page + 1%></span></a>
<% } else { %>
<a ref="<%= loaded_page %>?page=<%=page %>"><span><%=page + 1%></span></a>
<% } %> <% } %>
</div>
<% if(pagination.includes(Number(current_page) + 1)) {%>
<a href="<%= loaded_page %>?page=<%= Number(current_page) + 1 %>" class="right button"><span>Next ></span></a>
<% } else {%>
<button class="right button disabled"><span>Next ></span></button>
<%}%>
</div>

View File

@ -0,0 +1,20 @@
<div class="post">
<a href="/post/<%= post.id %>" class="title"><%= post.title ? post.title : "Untitled Post" %></a>
<div class="authors">By <a href="/author/<%= post.owner.id %>"><%= post.owner.display_name || post.owner.username %></a></div>
<div class="description"><%= post.description %></div>
<div class="badges">
<div class="info">
<div class="info-blip icon publish-date"><%= post.publish_date ? post.publish_date.toLocaleString('en-US', { dateStyle:'medium'}) : "Unknown Publish Date" %></div>
<div class="info-blip icon reading-time">Null minute read</div>
<% if (logged_in_user) { %>
<!-- -->
<div class="info-blip visibility-flag <%= post.visibility.toLowerCase() %>"><span><%= post.visibility %></span></div>
<!-- -->
<% if (new Date(post.publish_date) > new Date() && post.visibility !== 'PRIVATE') {%>
<div class="info-blip visibility-flag scheduled"><span>Scheduled</span></div>
<% } %>
<!-- -->
<% } %>
</div>
</div>
</div>

View File

@ -0,0 +1,31 @@
<div class="rich-text-editor">
<div class="controls">
<div class="left">
<a id="insert-h1"><span>H1</span></a>
<a id="insert-h2"><span>H2</span></a>
<a id="insert-h3"><span>H3</span></a>
<a id="insert-h4"><span>H4</span></a>
<a id="insert-underline">
<span><u>U</u></span>
</a>
<a id="insert-italics">
<span><i>i</i></span>
</a>
<a id="insert-bold">
<span><strong>B</strong></span>
</a>
<a id="insert-strike">
<span><del>S</del></span>
</a>
<a id="insert-sup">
<span><sup>Sup</sup></span>
</a>
</div>
<div class="right">
<a id="insert-sidebyside"><img src="/img/textarea/sidebyside.svg" /></a>
<a id="insert-video"><img src="/img/textarea/video.svg" /></a>
</div>
</div>
<textarea id="<%= text_selector %>"><%= prefill %></textarea>
</div>
<script defer src="/js/richTextEditor.js"></script>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="../css/post.css" />
<link rel="stylesheet" type="text/css" href="../css/generic.css" />
<script src="/js/generic.js"></script>
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<%if(logged_in_user) {%>
<div class="page-center">
<div class="horizontal-button-container">
<button onclick="deletePost()" href="#" class="button bad"><span>Delete Post</span></button>
<button onclick="window.location = '/post/<%=blog_post.id %>/edit'" class="button caution"><span>Edit Post</span></button>
</div>
</div>
<%}%>
<div class="page-center">
<div class="title"><%= blog_post.title %></div>
<%- blog_post.content %>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/post.js"></script>

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/css/newPost.css" />
<link rel="stylesheet" type="text/css" href="/css/generic.css" />
<script src="/js/generic.js"></script>
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<div class="page-center">
<div class="page-title">New Post</div>
<div class="info-container">
<div class="title">Title</div>
<input id="post-title" value="<%= existing_blog.title %>" type="text" />
</div>
<div class="info-container">
<div class="title">Description</div>
<textarea id="post-description"><%= existing_blog.description %></textarea>
</div>
<div class="info-container">
<div class="title">Tags</div>
<input id="post-tags" type="text" value="<%= existing_blog.tags %>" />
</div>
<div class="separator"></div>
<div class="info-container">
<div class="title">Content</div>
<%- include("partials/richTextEditor.ejs", {text_selector: 'post-content', prefill: existing_blog.raw_content}) %>
</div>
<div class="info-container">
<div class="title">Post Date</div>
<div class="side-by-side-info">
<input id="date" type="date" value="<%= existing_blog.publish_date %>" />
<input id="time" type="time" value="<%= existing_blog.publish_time %>" />
</div>
</div>
<div class="info-container">
<div class="side-by-side-info">
<button onclick="publish('PRIVATE')" class="button caution">Private</button>
<button onclick="publish('UNLISTED')" class="button caution">Unlisted</button>
<button onclick="publish('PUBLISHED')" class="button">Publish</button>
</div>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/newPost.js"></script>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/css/index.css" />
<link rel="stylesheet" type="text/css" href="/css/generic.css" />
<script src="/js/generic.js"></script>
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<div class="page-center">
<div class="search">
<div class="action">
<input type="text" placeholder="Search..." />
<button class="button" onclick="search()"><span>Search</span></button>
</div>
</div>
<div class="post-list-container">
<div class="post-list full">
<% for(post of blog_list) { %>
<!-- -->
<%- include("partials/post.ejs", {post:post}) %>
<!-- -->
<% } %>
</div>
</div>
<%- include("partials/pagination.ejs") %>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
<script defer src="/js/postSearch.js"></script>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="/css/login.css" />
<link rel="stylesheet" type="text/css" href="/css/generic.css" />
<script src="/js/generic.js"></script>
<title>Yet-Another-Blog</title>
</head>
<body>
<%- include("partials/header.ejs") %>
<div class="page">
<div class="page-center">
<div class="page-modal">
<div class="title">Register</div>
<div class="login-part">
<div class="name">Username:</div>
<input id="username" type="text" />
</div>
<div class="login-part">
<div class="name">Password:</div>
<input id="password" type="password" />
</div>
<div class="action-container">
<button onclick="requestRegister()" class="button"><span>Register</span></button>
<a href="/login">Login</a>
</div>
</div>
</div>
</div>
<%- include("partials/footer.ejs") %>
</body>
</html>
<script defer src="/js/login.js"></script>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M306-394q-17 0-28.5-11.5T266-434q0-17 11.5-28.5T306-474q17 0 28.5 11.5T346-434q0 17-11.5 28.5T306-394Zm177 0q-17 0-28.5-11.5T443-434q0-17 11.5-28.5T483-474q17 0 28.5 11.5T523-434q0 17-11.5 28.5T483-394Zm170 0q-17 0-28.5-11.5T613-434q0-17 11.5-28.5T653-474q17 0 28.5 11.5T693-434q0 17-11.5 28.5T653-394ZM180-80q-24 0-42-18t-18-42v-620q0-24 18-42t42-18h65v-60h65v60h340v-60h65v60h65q24 0 42 18t18 42v620q0 24-18 42t-42 18H180Zm0-60h600v-430H180v430Zm0-490h600v-130H180v130Zm0 0v-130 130Z"/></svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M308-140h344v-127q0-72-50-121.5T480-438q-72 0-122 49.5T308-267v127Zm172-382q72 0 122-50t50-122v-126H308v126q0 72 50 122t122 50ZM160-80v-60h88v-127q0-71 40-129t106-84q-66-27-106-85t-40-129v-126h-88v-60h640v60h-88v126q0 71-40 129t-106 85q66 26 106 84t40 129v127h88v60H160Zm320-60Zm0-680Z"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M320-240 80-480l240-240 57 57-184 184 183 183-56 56Zm320 0-57-57 184-184-183-183 56-56 240 240-240 240Z"/></svg>

After

Width:  |  Height:  |  Size: 209 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M140-120q-24 0-42-18t-18-42v-489h60v489h614v60H140Zm169-171q-24 0-42-18t-18-42v-489h671v489q0 24-18 42t-42 18H309Zm0-60h551v-429H309v429Zm86-131h168v-215H395v215Zm211 0h168v-88H606v88Zm0-127h168v-88H606v88ZM309-351v-429 429Z"/></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M194.956-120Q164-120 142-142.044q-22-22.045-22-53Q120-226 142.044-248q22.045-22 53-22Q226-270 248-247.956q22 22.045 22 53Q270-164 247.956-142q-22.045 22-53 22ZM710-120q0-123-46-229.5T537-537q-81-81-187.575-127Q242.85-710 120-710v-90q142 0 265 53t216 146q93 93 146 216t53 265h-90Zm-258 0q0-70-25.798-131.478Q400.404-312.956 355-360q-45-47-105.027-73.5Q189.945-460 120-460v-90q89 0 165.5 33.5t133.643 92.425q57.143 58.924 90 137Q542-209 542-120h-90Z"/></svg>

After

Width:  |  Height:  |  Size: 553 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="m239-160 40-159H120l15-60h159l51-202H186l15-60h159l39-159h59l-39 159h203l39-159h59l-39 159h159l-15 60H666l-51 202h159l-15 60H600l-40 159h-59l40-159H338l-40 159h-59Zm114-219h203l51-202H404l-51 202Z"/></svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@ -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 @@
<path
d="M 193,-320 0,-513 l 193,-193 42,42 -121,121 h 316 v 60 H 114 l 121,121 z m 414,-254 -42,-42 121,-121 H 370 v -60 h 316 l -121,-121 42,-42 193,193 z"
id="path1"
style="fill:#f9f9f9" />
style="fill:#000000" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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 @@
<path
d="M 303,-390 570,-560 303,-730 Z m 97,230 q -82,0 -155,-31.5 -73,-31.5 -127.5,-86 Q 63,-332 31.5,-405 0,-478 0,-560 q 0,-83 31.5,-156 31.5,-73 86,-127 54.5,-54 127.5,-85.5 73,-31.5 155,-31.5 83,0 156,31.5 73,31.5 127,85.5 54,54 85.5,127 31.5,73 31.5,156 0,82 -31.5,155 -31.5,73 -85.5,127.5 -54,54.5 -127,86 -73,31.5 -156,31.5 z m 0,-60 q 142,0 241,-99.5 99,-99.5 99,-240.5 0,-142 -99,-241 -99,-99 -241,-99 -141,0 -240.5,99 -99.5,99 -99.5,241 0,141 99.5,240.5 Q 259,-220 400,-220 Z m 0,-340 z"
id="path1"
style="fill:#f9f9f9" />
style="fill:#000000" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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) {
}
}

View File

@ -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) {
}
}

View File

@ -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,

View File

@ -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}`;
}
}

View File

@ -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() {}

View File

@ -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}`;

View File

@ -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}`;
}
}

View File

@ -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"
}
}

View File

@ -14,42 +14,42 @@ model User {
id String @id @unique @default(uuid())
username String @unique
password String
display_name String?
role Role @default(USER)
group String?
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[]
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
}

24
yab.js
View File

@ -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);