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
|
@ -4,6 +4,7 @@ const sharp = require("sharp");
|
|||
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsCommand, DeleteObjectsCommand } = require("@aws-sdk/client-s3");
|
||||
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||
let s3;
|
||||
const crypto = require("crypto");
|
||||
const md = require("markdown-it")()
|
||||
.use(require("markdown-it-underline"))
|
||||
.use(require("markdown-it-footnote"))
|
||||
|
@ -21,23 +22,19 @@ let settings = {
|
|||
HIDE_LOGIN: false,
|
||||
BLOG_UPLOADING: false,
|
||||
|
||||
CD_RSS: false,
|
||||
CD_AP: false,
|
||||
CD_RSS: true,
|
||||
CD_JSON: true,
|
||||
|
||||
WEBSITE_NAME: "",
|
||||
PLAUSIBLE_URL: "",
|
||||
|
||||
USER_MINIMUM_PASSWORD_LENGTH: 7,
|
||||
|
||||
BLOG_MINIMUM_TITLE_LENGTH: 7,
|
||||
BLOG_MINIMUM_DESCRIPTION_LENGTH: 7,
|
||||
BLOG_MINIMUM_CONTENT_LENGTH: 7,
|
||||
theme: "default",
|
||||
};
|
||||
let use_s3_storage = false;
|
||||
let groups = [];
|
||||
_initS3Storage();
|
||||
_getSettings();
|
||||
_getGroups();
|
||||
|
||||
// Checks to see if S3 storage is set
|
||||
function _initS3Storage() {
|
||||
|
@ -60,23 +57,20 @@ function _initS3Storage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function registerUser(username, password, options) {
|
||||
let user_database_entry;
|
||||
let user_profile_database_entry;
|
||||
// Users
|
||||
async function newUser({ username, password, role } = {}) {
|
||||
if (!username) return _r(false, "Username not specified");
|
||||
if (!password) return _r(false, "Password not specified");
|
||||
|
||||
// Create the entry in the database
|
||||
// Create the account
|
||||
try {
|
||||
user_database_entry = await prisma.user.create({ data: { username: username, password: password, ...options } });
|
||||
user_database_entry = await prisma.user.create({ data: { username: username, password: password, role: role } });
|
||||
} catch (e) {
|
||||
let message;
|
||||
|
||||
if (e.code === "P2002") message = "Username already exists";
|
||||
else message = "Unknown error";
|
||||
|
||||
let message = "Unknown error";
|
||||
return { success: false, message: message };
|
||||
}
|
||||
|
||||
// Create a user profile page
|
||||
// Create the profile page and link
|
||||
try {
|
||||
user_profile_database_entry = await prisma.profilePage.create({ data: { owner: { connect: { id: user_database_entry.id } } } });
|
||||
} catch (e) {
|
||||
|
@ -84,27 +78,86 @@ async function registerUser(username, password, options) {
|
|||
}
|
||||
|
||||
// Master user was created; server initialized
|
||||
postSetting("SETUP_COMPLETE", true);
|
||||
|
||||
// User has been successfully created
|
||||
return { success: true, message: `Successfully created ${username}` };
|
||||
editSetting({ name: "SETUP_COMPLETE", value: true });
|
||||
}
|
||||
async function getUser({ user_id, username, include_password = false }) {
|
||||
if (!username && !user_id) return _r(false, "Either a user_id or username is needed.");
|
||||
|
||||
let user;
|
||||
|
||||
if (user_id) user = await prisma.user.findUnique({ where: { id: user_id } });
|
||||
else if (username) user = await prisma.user.findUnique({ where: { username: username } });
|
||||
|
||||
if (!user) return _r(false, "No matching user");
|
||||
|
||||
// Delete the password from responses
|
||||
if (!include_password) delete user.password;
|
||||
|
||||
return { success: true, data: user };
|
||||
}
|
||||
async function editUser({ requester_id, user_id, user_content }) {
|
||||
let user = await getUser({ user_id: user_id });
|
||||
if (!user.success) return _r(false, "User not found");
|
||||
user = user.data;
|
||||
|
||||
// TODO:
|
||||
// If there was a role change, see if the acting user can make these changes
|
||||
|
||||
// TODO:
|
||||
// If there was a password change,
|
||||
// check to see if the user can make these changes
|
||||
// Hash the password
|
||||
|
||||
// FIXME: Not secure. ASAP!
|
||||
let formatted = {};
|
||||
formatted[user_content.setting_name] = user_content.value;
|
||||
|
||||
await prisma.user.update({ where: { id: user.id }, data: formatted });
|
||||
return _r(true);
|
||||
}
|
||||
async function deleteUser({ user_id }) {
|
||||
if (!user_id) return _r(false, "User_id not specified.");
|
||||
|
||||
await prisma.user.delete({ where: { id: user_id } }); // TODO: Test
|
||||
return _r(true, `User ${user_id} deleted`);
|
||||
}
|
||||
|
||||
// Posts
|
||||
async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, page = 0, search_title = false, search_content = false, search_tags = false, search }) {
|
||||
// If we have an ID, we want a single post
|
||||
if (id) {
|
||||
// Get the post by the id
|
||||
let post = await prisma.blogPost.findUnique({ where: { id: id }, include: { owner: true } });
|
||||
if (!post) return { success: false, message: "Post does not exist" };
|
||||
async function newPost({ requester_id }) {
|
||||
// TODO: Validate request (Does user have perms?)
|
||||
// TODO: Does server allow new posts?
|
||||
|
||||
// Render the post
|
||||
const rendered_post = await _renderPost(post, true);
|
||||
// Find if user already has a draft
|
||||
let existing_post = await prisma.post.findFirst({ where: { owner: { id: requester_id }, visibility: "DRAFT" } });
|
||||
if (existing_post) return existing_post.id;
|
||||
|
||||
// Return the post with valid image urls
|
||||
return { data: rendered_post, success: true };
|
||||
const post = await prisma.post.create({ data: { owner: { connect: { id: requester_id } } } });
|
||||
|
||||
return post.id;
|
||||
}
|
||||
async function getPost({ requester_id, post_id, visibility = "PUBLISHED" } = {}, { search, search_title, search_content, search_tags } = {}, { limit = 10, page = 0, pagination = true } = {}) {
|
||||
// Get a single post
|
||||
if (post_id) {
|
||||
let post;
|
||||
post = await prisma.post.findUnique({ where: { id: post_id }, include: { owner: true, tags: true } });
|
||||
if (!post) return _r(false, "Post does not exist");
|
||||
post = _stripPrivatePost(post);
|
||||
|
||||
// Tags
|
||||
let post_tags = [];
|
||||
post.raw_tags = [];
|
||||
post.tags.forEach((tag) => {
|
||||
post_tags.push(tag.name);
|
||||
post.raw_tags.push();
|
||||
});
|
||||
post.tags = post_tags;
|
||||
|
||||
// Render post
|
||||
return { success: true, data: await _renderPost(post) };
|
||||
}
|
||||
|
||||
// Otherwise build WHERE_OBJECT using data we do have
|
||||
let rendered_post_list = [];
|
||||
let post_list = [];
|
||||
let where_object = {
|
||||
OR: [
|
||||
// Standard discovery: Public, and after the publish date
|
||||
|
@ -123,7 +176,7 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag
|
|||
|
||||
// User owns the post
|
||||
{
|
||||
ownerid: owner_id,
|
||||
ownerid: requester_id,
|
||||
},
|
||||
],
|
||||
|
||||
|
@ -133,193 +186,210 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Build the "where_object" object
|
||||
if (search) {
|
||||
if (search_tags) where_object["AND"][0]["OR"].push({ tags: { hasSome: [search?.toLowerCase()] } });
|
||||
if (search_tags) where_object["AND"][0]["OR"].push({ tags: { some: { name: search?.toLowerCase() } } });
|
||||
if (search_title) where_object["AND"][0]["OR"].push({ title: { contains: search, mode: "insensitive" } });
|
||||
if (search_content) where_object["AND"][0]["OR"].push({ content: { contains: search, mode: "insensitive" } });
|
||||
}
|
||||
|
||||
// Execute search
|
||||
const blog_posts = await prisma.blogPost.findMany({
|
||||
let posts = await prisma.post.findMany({
|
||||
where: where_object,
|
||||
take: limit,
|
||||
skip: Math.max(page, 0) * limit,
|
||||
include: { owner: true },
|
||||
include: { owner: true, tags: true },
|
||||
orderBy: [{ publish_date: "desc" }, { created_date: "desc" }],
|
||||
});
|
||||
|
||||
// Render each of the posts in the list
|
||||
for (post of blog_posts) {
|
||||
rendered_post_list.push(await _renderPost(post, true));
|
||||
for (post of posts) {
|
||||
post = _stripPrivatePost(post);
|
||||
post = await _renderPost(post);
|
||||
post_list.push(post);
|
||||
|
||||
let post_tags = [];
|
||||
post.tags.forEach((tag) => post_tags.push(tag.name));
|
||||
post.tags = post_tags;
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
let pagination = await prisma.blogPost.count({
|
||||
let post_count = await prisma.post.count({
|
||||
where: where_object,
|
||||
});
|
||||
return { data: rendered_post_list, pagination: _getNavigationList(page, Math.ceil(pagination / limit)), success: true };
|
||||
}
|
||||
async function getAuthorPage({ author_id }) {
|
||||
// Get the post by the id
|
||||
|
||||
let post = await prisma.profilePage.findUnique({ where: { ownerid: author_id }, include: { owner: true } });
|
||||
if (!post) return { success: false, message: "Post does not exist" };
|
||||
return { data: post_list, pagination: _getNavigationList(page, Math.ceil(post_count / limit)), success: true };
|
||||
|
||||
// Render the post
|
||||
const rendered_post = await _renderPost(post, true);
|
||||
function _getNavigationList(current_page, max_page) {
|
||||
current_page = Number(current_page);
|
||||
max_page = Number(max_page);
|
||||
|
||||
// Return the post with valid image urls
|
||||
return { data: rendered_post, success: true };
|
||||
}
|
||||
async function getUser({ id, username } = {}) {
|
||||
let user;
|
||||
if (id) user = await prisma.user.findUnique({ where: { id: id } });
|
||||
else if (username) user = await prisma.user.findUnique({ where: { username: username } });
|
||||
|
||||
if (!user) return { success: false, message: "No matching user" };
|
||||
else return { success: true, data: user };
|
||||
}
|
||||
async function postBlog(blog_post, owner_id) {
|
||||
const user = await getUser({ id: owner_id });
|
||||
// Check if user has permissions to upload a blog post
|
||||
|
||||
if (user.data.role !== "ADMIN" && user.data.role !== "AUTHOR") return { success: false, message: "User is not permitted" };
|
||||
|
||||
// Create object without image data to store in the database
|
||||
let blog_post_formatted = {
|
||||
title: blog_post.title,
|
||||
description: blog_post.description,
|
||||
content: blog_post.content,
|
||||
visibility: blog_post.visibility,
|
||||
publish_date: blog_post.publish_date,
|
||||
tags: blog_post.tags,
|
||||
};
|
||||
|
||||
// Save to database
|
||||
const database_blog = await prisma.blogPost.create({ data: { ...blog_post_formatted, owner: { connect: { id: owner_id } } } });
|
||||
|
||||
// Init image vars
|
||||
let uploaded_images = [];
|
||||
let uploaded_thumbnail = "DEFAULT";
|
||||
|
||||
// For Each image, upload to S3
|
||||
if (blog_post.images) {
|
||||
for (let i = 0; blog_post.images.length > i; i++) {
|
||||
const image = blog_post.images[i];
|
||||
const image_data = Buffer.from(image.data_blob.split(",")[1], "base64");
|
||||
const name = await _uploadImage(database_blog.id, "blog", false, image_data, image.id);
|
||||
if (name) uploaded_images.push(name);
|
||||
}
|
||||
const pageList = [current_page - 2, current_page - 1, current_page, current_page + 1, current_page + 2].filter((num) => num >= 0 && num < max_page);
|
||||
return pageList.slice(0, 5);
|
||||
}
|
||||
|
||||
// Upload thumbnail to S3
|
||||
if (blog_post.thumbnail) {
|
||||
const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64");
|
||||
const name = await _uploadImage(database_blog.id, "blog", true, image_data, blog_post.thumbnail.id);
|
||||
if (name) uploaded_thumbnail = name;
|
||||
}
|
||||
|
||||
// Update the blog post to include references to our images
|
||||
await prisma.blogPost.update({ where: { id: database_blog.id }, data: { images: uploaded_images, thumbnail: uploaded_thumbnail } });
|
||||
return { success: true, blog_id: database_blog.id };
|
||||
}
|
||||
async function deleteBlog(blog_id, requester_id) {
|
||||
const user = await getUser({ id: requester_id });
|
||||
const post = await getBlog({ id: blog_id });
|
||||
|
||||
if (!post.success) return { success: false, message: post.message || "Post does not exist" };
|
||||
|
||||
let can_delete = post.data.owner.id === user.data.id || user.data.role === "ADMIN";
|
||||
|
||||
if (can_delete) {
|
||||
await prisma.blogPost.delete({ where: { id: post.data.id } });
|
||||
_deleteS3Directory(post.data.id, "blog");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return { success: false, message: "Action not permitted" };
|
||||
}
|
||||
async function updateBlog(blog_post, requester_id) {
|
||||
const user = await getUser({ id: requester_id });
|
||||
const post = await getBlog({ id: blog_post.id, raw: true });
|
||||
async function editPost({ requester_id, post_id, post_content }) {
|
||||
let user = await getUser({ user_id: requester_id });
|
||||
let post = await getPost({ post_id: post_id });
|
||||
let publish_date = null;
|
||||
|
||||
delete blog_post.id;
|
||||
if (!user.success) return _r(false, post.message || "User not found");
|
||||
user = user.data;
|
||||
if (!post.success) return _r(false, post.message || "Post not found");
|
||||
post = post.data;
|
||||
|
||||
if (!post.success) return { success: false, message: post.message || "Post not found" };
|
||||
|
||||
let can_update = post.data.owner.id === user.data.id || user.data.role === "ADMIN";
|
||||
|
||||
if (!can_update) return { success: false, message: "User not permitted" };
|
||||
// Check to see if the requester can update the post
|
||||
// TODO: Permissions
|
||||
let can_update = post.owner.id === user.id || user.role === "ADMIN";
|
||||
|
||||
// FIXME: Unsure if this actually works
|
||||
// Check if we already have a formatted publish date
|
||||
if (typeof blog_post.publish_date !== "object") {
|
||||
const [year, month, day] = blog_post.date.split("-");
|
||||
const [hour, minute] = blog_post.time.split(":");
|
||||
if (typeof post.publish_date !== "object") {
|
||||
const [year, month, day] = post.date.split("-");
|
||||
const [hour, minute] = post.time.split(":");
|
||||
publish_date = new Date(year, month - 1, day, hour, minute);
|
||||
}
|
||||
|
||||
let blog_post_formatted = {
|
||||
title: blog_post.title,
|
||||
description: blog_post.description,
|
||||
content: blog_post.content,
|
||||
visibility: blog_post.unlisted ? "UNLISTED" : "PUBLISHED",
|
||||
publish_date: publish_date || blog_post.publish_date,
|
||||
tags: blog_post.tags,
|
||||
};
|
||||
// Handle tags ----
|
||||
let database_tag_list = [];
|
||||
const existing_tags = post.tags?.map((tag) => ({ name: tag })) || [];
|
||||
|
||||
await prisma.blogPost.update({ where: { id: post.data.id }, data: blog_post_formatted });
|
||||
// Add new tags
|
||||
for (let tag_index = 0; post_content.tags.length > tag_index; tag_index++) {
|
||||
let tag = post_content.tags[tag_index];
|
||||
|
||||
let uploaded_images = [];
|
||||
let uploaded_thumbnail = "DEFAULT";
|
||||
// Check to see if tag exists, create if necessary,
|
||||
let database_tag = await prisma.tag.upsert({ where: { name: tag }, update: {}, create: { name: tag } });
|
||||
|
||||
// For Each image, upload to S3
|
||||
if (blog_post.images) {
|
||||
for (let i = 0; blog_post.images.length > i; i++) {
|
||||
const image = blog_post.images[i];
|
||||
const image_data = Buffer.from(image.data_blob.split(",")[1], "base64");
|
||||
const name = await _uploadImage(post.id, "blog", false, image_data, image.id);
|
||||
if (name) uploaded_images.push(name);
|
||||
}
|
||||
database_tag_list.push(database_tag);
|
||||
}
|
||||
|
||||
let data_to_update = {
|
||||
images: [...post.data.raw_images, ...uploaded_images],
|
||||
// Rebuild the post to save
|
||||
let post_formatted = {
|
||||
title: post_content.title,
|
||||
description: post_content.description,
|
||||
content: post_content.content,
|
||||
visibility: post_content.visibility || "PRIVATE",
|
||||
publish_date: publish_date || post_content.publish_date,
|
||||
tags: { disconnect: [...existing_tags], connect: [...database_tag_list] },
|
||||
media: [...post.raw_media, ...post_content.media],
|
||||
};
|
||||
|
||||
if (blog_post.thumbnail) {
|
||||
const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64");
|
||||
const name = await _uploadImage(post.data.id, "blog", true, image_data, blog_post.thumbnail.id);
|
||||
if (name) uploaded_thumbnail = name;
|
||||
// Save the updated post to the database
|
||||
await prisma.post.update({ where: { id: post.id }, data: post_formatted });
|
||||
|
||||
data_to_update.thumbnail = uploaded_thumbnail;
|
||||
}
|
||||
// Prune the post to save on storage
|
||||
await pruneMedia({ parent_id: post_id, parent_type: "posts" });
|
||||
|
||||
await prisma.blogPost.update({ where: { id: post.data.id }, data: data_to_update });
|
||||
return _r(true);
|
||||
}
|
||||
async function deletePost({ requester_id, post_id }) {
|
||||
let user = await getUser({ user_id: requester_id });
|
||||
let post = await getPost({ post_id: post_id });
|
||||
|
||||
if (!user.success) return { success: false, message: user.message || "User does not exist" };
|
||||
user = user.data;
|
||||
|
||||
if (!post.success) return { success: false, message: post.message || "Post does not exist" };
|
||||
post = post.data;
|
||||
|
||||
let can_delete = post.owner.id === user.id || user.role === "ADMIN";
|
||||
|
||||
if (!can_delete) return { success: false, message: "Action not permitted" };
|
||||
|
||||
await prisma.post.delete({ where: { id: post.id } });
|
||||
_deleteS3Directory(post.id, "post");
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
async function deleteImage(image, requester_id) {
|
||||
const user = await getUser({ id: requester_id });
|
||||
const post = await getBlog({ id: image.parent, raw: true });
|
||||
// User Profiles
|
||||
async function getBiography({ requester_id, author_id }) {
|
||||
if (!author_id) return _r(false, "No Author specified.");
|
||||
let post = await prisma.profilePage.findFirst({ where: { ownerid: author_id }, include: { owner: true } });
|
||||
|
||||
// Check if post exists
|
||||
if (!post) return { success: false, message: "Post does not exist" };
|
||||
// Check if it is private
|
||||
// TODO
|
||||
|
||||
// Check for permissions
|
||||
if (post.owner.id !== user.data.id || user.data.role !== "ADMIN") return { success: false, message: "User is not permitted" };
|
||||
// HACK:
|
||||
// When we render the post and reading from S3, we want the post id
|
||||
// The problem is when a user views the biography page, the page shows the account id opposed to the "profile page" id.
|
||||
// This causes a incorrect parent_id value and an incorrect key.
|
||||
// Replace the "id" to the value it's expecting.
|
||||
const original_post_id = post.id;
|
||||
let rendering_formatted_post = {};
|
||||
|
||||
let image_index = post.raw_images.indexOf(image.id);
|
||||
rendering_formatted_post = post;
|
||||
rendering_formatted_post.id = author_id;
|
||||
|
||||
post.raw_images.splice(image_index, 1);
|
||||
// Render
|
||||
post = _stripPrivatePost(post);
|
||||
post = await _renderPost(rendering_formatted_post);
|
||||
|
||||
await prisma.blogPost.update({ where: { id: post.id }, data: { images: post.raw_images } });
|
||||
post.id = original_post_id;
|
||||
|
||||
return { success: true, data: post };
|
||||
}
|
||||
async function updateBiography({ requester_id, author_id, biography_content }) {
|
||||
let user = await getUser({ user_id: requester_id });
|
||||
let biography = await getBiography({ author_id: author_id });
|
||||
|
||||
if (!user.success) return _r(false, user.message || "Author not found");
|
||||
user = user.data;
|
||||
|
||||
if (!biography.success) return _r(false, biography.message || "Post not found");
|
||||
biography = biography.data;
|
||||
|
||||
let can_update = biography.owner.id === user.id || user.role === "ADMIN";
|
||||
if (!can_update) return _r(false, "User not permitted");
|
||||
|
||||
let formatted = {
|
||||
content: biography_content.content,
|
||||
media: [...biography.raw_media, ...biography_content.media],
|
||||
};
|
||||
|
||||
await prisma.profilePage.update({ where: { id: biography.id }, data: formatted });
|
||||
|
||||
return _r(true);
|
||||
}
|
||||
async function uploadMedia({ parent_id, parent_type, file_buffer, content_type }) {
|
||||
if (!use_s3_storage) return null;
|
||||
const content_name = crypto.randomUUID();
|
||||
let maximum_image_resolution = { width: 1920, height: 1080 };
|
||||
|
||||
// Images
|
||||
const compressed_image = await sharp(Buffer.from(file_buffer.split(",")[1], "base64"), { animated: true })
|
||||
.resize({ ...maximum_image_resolution, withoutEnlargement: true, fit: "inside" })
|
||||
.webp({ quality: 90, animated: true })
|
||||
.toBuffer();
|
||||
|
||||
let extension;
|
||||
let s3_content_type;
|
||||
|
||||
if (content_type.includes("image/")) {
|
||||
extension = ".webp";
|
||||
s3_content_type = "image/webp";
|
||||
}
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.S3_BUCKET_NAME,
|
||||
Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${content_name}${extension}`,
|
||||
Body: compressed_image,
|
||||
ContentType: s3_content_type,
|
||||
};
|
||||
|
||||
const command = new PutObjectCommand(params);
|
||||
await s3.send(command);
|
||||
|
||||
return content_name + extension;
|
||||
}
|
||||
async function getMedia({ parent_id, parent_type, file_name }) {
|
||||
if (!use_s3_storage) return null;
|
||||
const params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${file_name}` };
|
||||
return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 });
|
||||
}
|
||||
|
||||
async function deleteMedia({ parent_id, parent_type, file_name }) {
|
||||
const request_params = {
|
||||
Bucket: process.env.S3_BUCKET_NAME,
|
||||
Key: `${process.env.ENVIRONMENT}/${image.parent_type}/${image.parent}/${image.id}.webp`,
|
||||
Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${file_name}`,
|
||||
};
|
||||
|
||||
const command = new DeleteObjectCommand(request_params);
|
||||
|
@ -327,40 +397,58 @@ async function deleteImage(image, requester_id) {
|
|||
|
||||
return { success: true };
|
||||
}
|
||||
async function _uploadImage(parent_id, parent_type, is_thumbnail, buffer, name) {
|
||||
if (!use_s3_storage) return null;
|
||||
let size = { width: 1920, height: 1080 };
|
||||
if (is_thumbnail) size = { width: 300, height: 300 };
|
||||
|
||||
const compressed_image = await sharp(buffer, { animated: true })
|
||||
.resize({ ...size, withoutEnlargement: true, fit: "inside" })
|
||||
.webp({ quality: 90, animated: true })
|
||||
.toBuffer();
|
||||
// This cleans up all unused and unreferenced media files.
|
||||
// NOTE: Only made for posts, as that is all that there is right now
|
||||
async function pruneMedia({ parent_id, parent_type }) {
|
||||
let post = await getPost({ post_id: parent_id });
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.S3_BUCKET_NAME,
|
||||
Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${name}.webp`,
|
||||
Body: compressed_image,
|
||||
ContentType: "image/webp",
|
||||
};
|
||||
if (!post.success) return { success: false, message: post.message || "Post does not exist" };
|
||||
post = post.data;
|
||||
|
||||
const command = new PutObjectCommand(params);
|
||||
await s3.send(command);
|
||||
// const total_number_of_media = post.raw_media.length;
|
||||
|
||||
return name;
|
||||
for (let media_index = 0; post.raw_media.length > media_index; media_index++) {
|
||||
if (!post.raw_content.includes(post.raw_media[media_index])) {
|
||||
// Delete the media off of the S3 server
|
||||
let delete_request = await deleteMedia({ parent_id: parent_id, parent_type: parent_type, file_name: post.raw_media[media_index] });
|
||||
if (!delete_request.success) continue;
|
||||
|
||||
// Remove from the list in the database
|
||||
await post.raw_media.splice(media_index, 1);
|
||||
// Save in the database
|
||||
await prisma.post.update({ where: { id: parent_id }, data: { media: post.raw_media } });
|
||||
|
||||
// Delete was successful, move the index back to account for new array length
|
||||
media_index--;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function _getImage(parent_id, parent_type, name) {
|
||||
if (!use_s3_storage) return null;
|
||||
let params;
|
||||
// Default image
|
||||
if (name === "DEFAULT") params = { Bucket: process.env.S3_BUCKET_NAME, Key: `defaults/thumbnail.webp` };
|
||||
// Named image
|
||||
else params = { Bucket: process.env.S3_BUCKET_NAME, Key: `${process.env.ENVIRONMENT}/${parent_type}/${parent_id}/${name}.webp` };
|
||||
|
||||
return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 3600 });
|
||||
async function getTags({ order = "count" } = {}) {
|
||||
if (order == "count") {
|
||||
return await prisma.tag.findMany({
|
||||
include: { _count: { select: { posts: true } } },
|
||||
where: {
|
||||
posts: {
|
||||
some: {},
|
||||
},
|
||||
},
|
||||
take: 15,
|
||||
orderBy: {
|
||||
posts: {
|
||||
_count: "desc",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Will be done automatically in the background
|
||||
async function deleteTag({ tag_id }) {}
|
||||
|
||||
async function _deleteS3Directory(id, type) {
|
||||
// logger.verbose(`Deleting entire S3 image directory`);
|
||||
// Erase database images from S3 server
|
||||
const folder_params = { Bucket: process.env.S3_BUCKET_NAME, Prefix: `${process.env.ENVIRONMENT}/${type}/${id}` };
|
||||
|
||||
|
@ -387,93 +475,82 @@ async function _deleteS3Directory(id, type) {
|
|||
// If there are more objects to delete (truncated result), recursively call the function again
|
||||
// if (listed_objects.IsTruncated) await emptyS3Directory(bucket, dir);
|
||||
}
|
||||
async function _renderPost(blog_post, raw, { post_type = "blog" } = {}) {
|
||||
if (raw) {
|
||||
// Had to do this, only God knows why.
|
||||
blog_post.raw_images = [];
|
||||
if (blog_post.images) blog_post.images.forEach((image) => blog_post.raw_images.push(image));
|
||||
|
||||
blog_post.raw_thumbnail = blog_post.thumbnail;
|
||||
blog_post.raw_content = blog_post.content;
|
||||
}
|
||||
async function _renderPost(post) {
|
||||
post.raw_media = [];
|
||||
post.raw_content = post.content;
|
||||
|
||||
if (blog_post.images) {
|
||||
// Get the image urls for the post
|
||||
for (i = 0; blog_post.images.length > i; i++) {
|
||||
blog_post.images[i] = await _getImage(blog_post.id, post_type, blog_post.images[i]);
|
||||
// For some reason Node does not like to set a variable and leave it.
|
||||
post.media.forEach((media) => post.raw_media.push(media));
|
||||
|
||||
if (post.media) {
|
||||
for (i = 0; post.media.length > i; i++) {
|
||||
post.media[i] = await getMedia({ parent_id: post.id, parent_type: "posts", file_name: post.media[i] });
|
||||
}
|
||||
}
|
||||
|
||||
// get thumbnail URL
|
||||
blog_post.thumbnail = await _getImage(blog_post.id, post_type, blog_post.thumbnail);
|
||||
|
||||
if (blog_post.content) {
|
||||
if (post.content) {
|
||||
// Render the markdown contents of the post
|
||||
blog_post.content = md.render(blog_post.content);
|
||||
post.content = md.render(post.content);
|
||||
|
||||
// Replace custom formatting with what we want
|
||||
blog_post.content = _format_blog_content(blog_post.content, blog_post.images);
|
||||
post.content = _formatBlogContent(post.content, post.media);
|
||||
}
|
||||
return post;
|
||||
|
||||
return blog_post;
|
||||
}
|
||||
function _format_blog_content(content, images) {
|
||||
// Replace Images
|
||||
const image_regex = /{image:([^}]+)}/g;
|
||||
function _formatBlogContent(content, media_list) {
|
||||
// Replace Images
|
||||
const image_regex = /{image:([^}]+)}/g;
|
||||
|
||||
// Replace Side-by-side
|
||||
const side_by_side = /{sidebyside}(.*?){\/sidebyside}/gs;
|
||||
// Replace Side-by-side
|
||||
const side_by_side = /{sidebyside}(.*?){\/sidebyside}/gs;
|
||||
|
||||
// Replace video links
|
||||
const video = /{video:([^}]+)}/g;
|
||||
// Replace video links
|
||||
const video = /{video:([^}]+)}/g;
|
||||
|
||||
content = content.replace(video, (match, inner_content) => {
|
||||
return `<div class='video-embed'><iframe src="${_getVideoEmbed(inner_content)}" frameborder="0" allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
|
||||
});
|
||||
content = content.replace(video, (match, inner_content) => {
|
||||
return `<div class='video-embed'><iframe src="${_getVideoEmbed(inner_content)}" frameborder="0" allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
|
||||
});
|
||||
|
||||
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>`;
|
||||
// Replace Images
|
||||
content = content.replace(image_regex, (match, image_name) => {
|
||||
for (media of media_list) {
|
||||
if (media.includes(image_name)) {
|
||||
return `<div class='image-container'><img src='${media}'></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown image (Image was probably deleted)
|
||||
return "";
|
||||
});
|
||||
|
||||
content = content.replace(side_by_side, (match, inner_content) => {
|
||||
return `<div class='side-by-side'>${inner_content}</div>`;
|
||||
});
|
||||
|
||||
// Finished formatting, return!
|
||||
return content;
|
||||
|
||||
function _getVideoEmbed(video_url) {
|
||||
// YouTube
|
||||
if (video_url.includes("youtu.be")) {
|
||||
return `https://youtube.com/embed/${video_url.split("/")[3]}`;
|
||||
}
|
||||
if (video_url.includes("youtube")) {
|
||||
let video_id = video_url.split("/")[3];
|
||||
video_id = video_id.split("watch?v=").pop();
|
||||
return `https://youtube.com/embed/${video_id}`;
|
||||
}
|
||||
|
||||
// Odysee
|
||||
if (video_url.includes("://odysee.com")) {
|
||||
let video_link = `https://${video_url.split("/")[2]}/$/embed/${video_url.split("/")[3]}/${video_url.split("/")[4]}`;
|
||||
return video_link;
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown image (Image was probably deleted)
|
||||
return "";
|
||||
});
|
||||
|
||||
content = content.replace(side_by_side, (match, inner_content) => {
|
||||
return `<div class='side-by-side'>${inner_content}</div>`;
|
||||
});
|
||||
|
||||
// Finished formatting, return!
|
||||
return content;
|
||||
|
||||
function _getVideoEmbed(video_url) {
|
||||
// YouTube
|
||||
if (video_url.includes("youtu.be")) {
|
||||
return `https://youtube.com/embed/${video_url.split("/")[3]}`;
|
||||
}
|
||||
if (video_url.includes("youtube")) {
|
||||
let video_id = video_url.split("/")[3];
|
||||
video_id = video_id.split("watch?v=").pop();
|
||||
return `https://youtube.com/embed/${video_id}`;
|
||||
}
|
||||
|
||||
// Odysee
|
||||
if (video_url.includes("://odysee.com")) {
|
||||
let video_link = `https://${video_url.split("/")[2]}/$/embed/${video_url.split("/")[3]}/${video_url.split("/")[4]}`;
|
||||
return video_link;
|
||||
}
|
||||
}
|
||||
}
|
||||
function _getNavigationList(current_page, max_page) {
|
||||
current_page = Number(current_page);
|
||||
max_page = Number(max_page);
|
||||
|
||||
const pageList = [current_page - 2, current_page - 1, current_page, current_page + 1, current_page + 2].filter((num) => num >= 0 && num < max_page);
|
||||
return pageList.slice(0, 5);
|
||||
}
|
||||
async function _getSettings() {
|
||||
// Go though each object key in our settings to get the value if it exists
|
||||
Object.keys(settings).forEach(async (key) => {
|
||||
|
@ -491,7 +568,7 @@ async function _getSettings() {
|
|||
return (settings[key] = value);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Replace
|
||||
async function getSetting(key, { parse = true }) {
|
||||
if (!settings[key]) return null;
|
||||
|
||||
|
@ -500,19 +577,44 @@ async function getSetting(key, { parse = true }) {
|
|||
}
|
||||
return settings[key];
|
||||
}
|
||||
// TODO: Replace
|
||||
async function postSetting(key, value) {
|
||||
try {
|
||||
if (!Object.keys(settings).includes(key)) return { success: false, message: "Setting not valid" };
|
||||
|
||||
await prisma.setting.upsert({ where: { id: key }, update: { value: value }, create: { id: key, value: value } });
|
||||
settings[key] = JSON.parse(value);
|
||||
try {
|
||||
settings[key] = JSON.parse(value);
|
||||
} catch {
|
||||
settings[key] = value;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
}
|
||||
async function _getGroups() {
|
||||
const group_list = await prisma.group.findMany();
|
||||
// TODO: Replace
|
||||
async function editSetting({ name, value }) {
|
||||
if (!Object.keys(settings).includes(name)) return _r(false, "Setting is not valid");
|
||||
|
||||
await prisma.setting.upsert({ where: { id: key }, update: { value: value }, create: { id: key, value: value } });
|
||||
try {
|
||||
settings[key] = JSON.parse(value);
|
||||
} catch {
|
||||
settings[key] = value;
|
||||
}
|
||||
|
||||
return _r(true);
|
||||
}
|
||||
module.exports = { settings, registerUser, getUser, getAuthorPage, postBlog, updateBlog, getBlog, deleteBlog, deleteImage, postSetting, getSetting };
|
||||
|
||||
function _stripPrivatePost(post) {
|
||||
if (!post) return;
|
||||
if (post.owner) delete post.owner.password;
|
||||
return post;
|
||||
}
|
||||
const _r = (s, m) => {
|
||||
return { success: s, message: m };
|
||||
};
|
||||
|
||||
module.exports = { settings, newUser, getUser, editUser, getPost, newPost, editPost, deletePost, getBiography, updateBiography, uploadMedia, getTags, postSetting, getSetting };
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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 = "/";
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,5 +0,0 @@
|
|||
<div class="blog-admin">
|
||||
<div class="horizontal-button-container">
|
||||
<a href="/blog/new"><span>New Post</span></a>
|
||||
</div>
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
|
@ -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%;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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}`;
|
||||
}
|
||||
}
|
|
@ -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() {}
|
|
@ -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}`;
|
|
@ -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}`;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -11,45 +11,45 @@ datasource db {
|
|||
}
|
||||
|
||||
model User {
|
||||
id String @id @unique @default(uuid())
|
||||
username String @unique
|
||||
password String
|
||||
id String @id @unique @default(uuid())
|
||||
username String @unique
|
||||
password String
|
||||
display_name String?
|
||||
|
||||
role Role @default(USER)
|
||||
group String?
|
||||
role Role @default(USER)
|
||||
|
||||
blog_posts BlogPost[]
|
||||
blog_posts Post[]
|
||||
profile_page ProfilePage?
|
||||
|
||||
@@index([username, role])
|
||||
}
|
||||
|
||||
model BlogPost {
|
||||
model Post {
|
||||
id String @id @unique @default(uuid())
|
||||
title String?
|
||||
description String?
|
||||
content String?
|
||||
thumbnail String?
|
||||
images String[]
|
||||
visibility PostStatus @default(UNLISTED)
|
||||
media String[]
|
||||
visibility PostStatus @default(DRAFT)
|
||||
owner User? @relation(fields: [ownerid], references: [id], onDelete: Cascade)
|
||||
ownerid String?
|
||||
|
||||
// Tags
|
||||
tags String[]
|
||||
tags Tag[]
|
||||
|
||||
// Dates
|
||||
publish_date DateTime?
|
||||
publish_date DateTime? @default(now())
|
||||
created_date DateTime @default(now())
|
||||
}
|
||||
|
||||
model ProfilePage {
|
||||
id String @id @unique @default(uuid())
|
||||
content String?
|
||||
images String[]
|
||||
visibility PostStatus @default(UNLISTED)
|
||||
owner User @relation(fields: [ownerid], references: [id], onDelete: Cascade)
|
||||
ownerid String @unique
|
||||
id String @id @unique @default(uuid())
|
||||
content String?
|
||||
media String[]
|
||||
visibility PostStatus @default(UNLISTED)
|
||||
owner User @relation(fields: [ownerid], references: [id], onDelete: Cascade)
|
||||
ownerid String @unique
|
||||
created_date DateTime @default(now())
|
||||
}
|
||||
|
||||
model Setting {
|
||||
|
@ -62,6 +62,18 @@ model Group {
|
|||
permissions String[]
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @unique @default(uuid())
|
||||
name String @unique
|
||||
type TagMode @default(NORMAL)
|
||||
posts Post[]
|
||||
}
|
||||
|
||||
enum TagMode {
|
||||
NORMAL
|
||||
ALIAS
|
||||
}
|
||||
|
||||
enum Role {
|
||||
LOCKED
|
||||
USER
|
||||
|
@ -69,6 +81,8 @@ enum Role {
|
|||
}
|
||||
|
||||
enum PostStatus {
|
||||
DRAFT
|
||||
PRIVATE
|
||||
UNLISTED
|
||||
PUBLISHED
|
||||
}
|
||||
|
|
24
yab.js
|
@ -12,10 +12,13 @@ const internal = require("./backend/core/internal_api");
|
|||
// Express settings
|
||||
app.set("view-engine", "ejs");
|
||||
app.set("views", path.join(__dirname, "frontend/views"));
|
||||
app.use(express.static(path.join(__dirname, "frontend/public")));
|
||||
app.use(express.json({ limit: "500mb" }));
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
// TODO: Does this persist previous themes? May cause security issues!
|
||||
const refreshTheme = (theme_name) => app.use(express.static(path.join(__dirname, `frontend/views/themes/${theme_name}`)));
|
||||
refreshTheme("default");
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: require("crypto").randomBytes(128).toString("base64"),
|
||||
|
@ -28,10 +31,12 @@ app.use(
|
|||
app.post("/login", checkNotAuthenticated, internal.postLogin);
|
||||
app.post("/register", checkNotAuthenticated, internal.postRegister);
|
||||
app.post("/setting", checkAuthenticated, internal.postSetting);
|
||||
app.post("/api/web/blog", checkAuthenticated, internal.postBlog);
|
||||
app.delete("/api/web/blog/image", checkAuthenticated, internal.deleteImage);
|
||||
app.delete("/api/web/blog", checkAuthenticated, internal.deleteBlog);
|
||||
app.patch("/api/web/blog", checkAuthenticated, internal.patchBlog);
|
||||
app.post("/api/web/image", checkAuthenticated, internal.postImage);
|
||||
app.delete("/api/web/post/image", checkAuthenticated, internal.deleteImage);
|
||||
app.delete("/api/web/post", checkAuthenticated, internal.deleteBlog);
|
||||
app.patch("/api/web/post", checkAuthenticated, internal.patchBlog);
|
||||
app.patch("/api/web/biography", checkAuthenticated, internal.patchBiography);
|
||||
app.patch("/api/web/user", checkAuthenticated, internal.patchUser);
|
||||
|
||||
// app.delete("/logout", page_scripts.logout);
|
||||
|
||||
|
@ -40,11 +45,12 @@ app.get("/", page_scripts.index);
|
|||
app.get("/login", page_scripts.login);
|
||||
app.get("/register", checkNotAuthenticated, page_scripts.register);
|
||||
app.get("/author/:author_id", page_scripts.author);
|
||||
app.get("/author/:author_id/edit", checkAuthenticated, page_scripts.authorEdit);
|
||||
app.get("/admin", checkAuthenticated, page_scripts.admin);
|
||||
app.get("/blog", page_scripts.blogList);
|
||||
app.get("/blog/new", checkAuthenticated, page_scripts.blogNew);
|
||||
app.get("/blog/:blog_id", page_scripts.blogSingle);
|
||||
app.get("/blog/:blog_id/edit", checkAuthenticated, page_scripts.blogEdit);
|
||||
app.get("/posts", page_scripts.blogList);
|
||||
app.get("/post/new", checkAuthenticated, page_scripts.blogNew);
|
||||
app.get("/post/:blog_id", page_scripts.blogSingle);
|
||||
app.get("/post/:blog_id/edit", checkAuthenticated, page_scripts.blogEdit);
|
||||
app.get("/atom", page_scripts.atom);
|
||||
app.get("/json", page_scripts.jsonFeed);
|
||||
|
||||
|
|