Admin page refresh. (#6)

Added settings and options.
Settings parsing catch.
Fix postSetting API responses.
Adjusted spinner visibility toggle.

Reviewed-on: #6
Co-authored-by: Armored-Dragon <forgejo3829105@armoreddragon.com>
Co-committed-by: Armored-Dragon <forgejo3829105@armoreddragon.com>
pull/2/head
Armored Dragon 2023-11-28 20:52:19 +00:00 committed by Armored Dragon
parent 94424df08f
commit 83da8100dc
11 changed files with 348 additions and 75 deletions

View File

@ -19,6 +19,12 @@ let settings = {
HIDE_LOGIN: false, HIDE_LOGIN: false,
BLOG_UPLOADING: false, BLOG_UPLOADING: false,
CD_RSS: false,
CD_AP: false,
WEBSITE_NAME: "",
PLAUSIBLE_URL: "",
USER_MINIMUM_PASSWORD_LENGTH: 7, USER_MINIMUM_PASSWORD_LENGTH: 7,
BLOG_MINIMUM_TITLE_LENGTH: 7, BLOG_MINIMUM_TITLE_LENGTH: 7,
@ -372,9 +378,18 @@ async function _getSettings() {
let found_value = await prisma.setting.findUnique({ where: { id: key } }); let found_value = await prisma.setting.findUnique({ where: { id: key } });
if (!found_value) return; if (!found_value) return;
return (settings[key] = JSON.parse(found_value.value)); let value;
// Parse JSON if possible
try {
value = JSON.parse(found_value.value);
} catch {
value = found_value.value;
}
return (settings[key] = value);
}); });
} }
async function getSetting(key, { parse = true }) { async function getSetting(key, { parse = true }) {
if (!settings[key]) return null; if (!settings[key]) return null;

View File

@ -43,9 +43,7 @@ async function postSetting(request, response) {
if (!user.success) return response.json({ success: false, message: user.message }); 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" }); if (user.data.role !== "ADMIN") return response.json({ success: false, message: "User is not permitted" });
await core.postSetting(request.body.setting_name, request.body.value); response.json(await core.postSetting(request.body.setting_name, request.body.value));
response.json({ success: true });
} }
async function deleteImage(req, res) { async function deleteImage(req, res) {
// TODO: Permissions for deleting image // TODO: Permissions for deleting image

View File

@ -2,7 +2,8 @@ const external = require("./core/external_api");
const core = require("./core/core"); const core = require("./core/core");
function getDefaults(req) { function getDefaults(req) {
return { logged_in_user: req.session.user, website_name: process.env.WEBSITE_NAME, settings: core.settings }; // 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 };
} }
async function index(request, response) { async function index(request, response) {

View File

@ -2,6 +2,7 @@ body {
margin: 0; margin: 0;
background-color: #111; background-color: #111;
color: white; color: white;
font-family: Verdana, Geneva, Tahoma, sans-serif;
} }
.header { .header {
@ -58,11 +59,18 @@ button:focus {
background-color: #122d57; background-color: #122d57;
} }
button.good { button.good,
a.good {
background-color: #015b01; background-color: #015b01;
} }
button.bad { button.yellow,
a.yellow {
background-color: #4a4a00;
}
button.bad,
a.bad {
background-color: #8e0000; background-color: #8e0000;
} }
@ -75,9 +83,78 @@ button.bad {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.page .horizontal-button-container button { .page .horizontal-button-container button,
width: 100px; .page .horizontal-button-container a {
width: 120px;
min-height: 30px; 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 { .hidden {
@ -86,7 +163,7 @@ button.bad {
@media screen and (max-width: 1010px) { @media screen and (max-width: 1010px) {
.page { .page {
width: 100%; width: 95%;
} }
} }
.container { .container {
@ -95,32 +172,73 @@ button.bad {
padding: 10px 20px; padding: 10px 20px;
box-sizing: border-box; box-sizing: border-box;
} }
.container .settings-group .settings-row { .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; display: flex;
flex-direction: row; flex-direction: row;
padding: 4px; min-height: 30px;
padding: 5px;
box-sizing: border-box; box-sizing: border-box;
} }
.container .settings-group .settings-row .label { .container .setting-row .setting-title {
font-size: 18px;
margin: auto auto auto 0; margin: auto auto auto 0;
} }
.container .settings-group .settings-row .button-group { .container .setting-row .setting-toggleable {
min-width: 150px;
display: flex; display: flex;
flex-direction: row;
margin: auto 0 auto auto; margin: auto 0 auto auto;
} }
.container .settings-group .settings-row .button-group .spinner { .container .setting-row .setting-toggleable .spinner {
margin: auto 10px auto auto; margin: auto 0 auto auto;
animation: spin 1s; animation: spin 1s;
animation-timing-function: linear; animation-timing-function: linear;
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
.container .settings-group .settings-row .button-group button { .container .setting-row .setting-toggleable input {
height: 30px; padding: 0;
padding: 0px 20px; margin: auto 0 auto auto;
width: 100px;
box-sizing: border-box;
text-align: center;
border-radius: 10px;
} }
.container .settings-group .settings-row:nth-child(even) { .container .setting-row .setting-toggleable input[type=text] {
background-color: rgba(0, 0, 0, 0.2); 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 { @keyframes spin {

View File

@ -6,35 +6,89 @@
padding: 10px 20px; padding: 10px 20px;
box-sizing: border-box; box-sizing: border-box;
.settings-group { .category-navigation {
.settings-row { width: 100%;
height: 30px;
display: flex;
margin-bottom: 15px;
button {
width: 100%;
margin-right: 5px;
border-radius: 5px;
text-decoration: none;
display: flex; display: flex;
flex-direction: row;
padding: 4px; span {
box-sizing: border-box; margin: auto;
.label { color: white;
margin: auto auto auto 0; }
}
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;
} }
.button-group { input {
display: flex; padding: 0;
flex-direction: row;
margin: auto 0 auto auto; margin: auto 0 auto auto;
.spinner { // width: 100%;
margin: auto 10px auto auto; width: 100px;
animation: spin 1s; box-sizing: border-box;
animation-timing-function: linear; text-align: center;
animation-iteration-count: infinite; border-radius: 10px;
} }
button {
height: 30px; input[type="text"] {
padding: 0px 20px; width: 350px;
} }
button {
width: 125px;
margin-left: auto;
height: 30px;
} }
} }
.settings-row:nth-child(even) { }
background-color: #00000033; .setting-row:nth-child(even) {
} background-color: #1c1c1c;
}
.setting-row:nth-child(odd) {
background-color: #191919;
} }
} }
@keyframes spin { @keyframes spin {

View File

@ -9,10 +9,11 @@ async function toggleState(setting_name, new_value, element_id) {
const response = await request("/setting", "POST", form); 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 // Check response for errors
if (response.body.success) { if (response.body.success) {
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.add("hidden");
// Update visual to reflect current setting // Update visual to reflect current setting
// Class // Class
const add_class = new_value ? "good" : "bad"; const add_class = new_value ? "good" : "bad";
@ -30,3 +31,42 @@ async function toggleState(setting_name, new_value, element_id) {
qs(`#${element_id}`).setAttribute("onclick", add_function); qs(`#${element_id}`).setAttribute("onclick", add_function);
} }
} }
async function updateValue(key_pressed, setting_name, new_value, element_id) {
if (key_pressed !== 13) return;
// Show spinner
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.remove("hidden");
const form = {
setting_name: setting_name,
value: new_value,
};
const response = await request("/setting", "POST", form);
qs(`#${element_id}`).parentNode.querySelector(".spinner").classList.add("hidden");
// TODO: On failure, notify the user
// Check response for errors
if (response.body.success) {
}
}
function toggleActiveCategory(category_id) {
// Pages ----------------
// Hide all pages
qsa(".category-page").forEach((page) => {
page.classList.add("hidden");
});
// Show requested page
qs(`#${category_id}`).classList.remove("hidden");
// Navigation bar -------
// Unactive all buttons
qsa(".category-navigation button").forEach((btn) => {
btn.classList.remove("active");
});
// Active current page
qs(`#${category_id}-nav-btn`).classList.add("active");
qs(`#${category_id}-nav-btn`).blur(); // Unfocus the button
}

View File

@ -2,6 +2,7 @@
const qs = (selector) => document.querySelector(selector); const qs = (selector) => document.querySelector(selector);
const qsa = (selector) => document.querySelectorAll(selector); const qsa = (selector) => document.querySelectorAll(selector);
// TODO: Try/Catch for failed requests
async function request(url, method, body) { async function request(url, method, body) {
const response = await fetch(url, { const response = await fetch(url, {
method: method, method: method,

View File

@ -13,35 +13,38 @@
<div class="page"> <div class="page">
<div class="container"> <div class="container">
<div class="settings-group"> <div class="category-navigation">
<h1>Accounts</h1> <button id="accounts-nav-btn" class="active" onclick="toggleActiveCategory('accounts')"><span>Accounts</span></button>
<div class="settings-row"> <button id="content-delivery-nav-btn" onclick="toggleActiveCategory('content-delivery')"><span>Content Delivery</span></button>
<div class="label">Account registration</div> <button id="groups-nav-btn" onclick="toggleActiveCategory('groups')"><span>Groups</span></button>
<div class="button-group"> <button id="yab-master-nav-btn" onclick="toggleActiveCategory('yab-master')"><span>YAB</span></button>
<div class="spinner hidden"> </div>
<div>↺</div> <div id="accounts" class="category-page">
</div> <!-- Account registration -->
<% if (!settings.ACCOUNT_REGISTRATION ) { %> <%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'ACCOUNT_REGISTRATION', name_pretty: 'Account registration', enabled:
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', true, this.id)" class="bad"><div>Disabled</div></button> settings.ACCOUNT_REGISTRATION}}) %>
<% } else { %> <!-- Hide login -->
<button id="registration_toggle" onclick="toggleState('ACCOUNT_REGISTRATION', false, this.id)" class="good"><div>Enabled</div></button> <%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'HIDE_LOGIN', name_pretty: 'Hide Login', enabled: settings.HIDE_LOGIN}}) %>
<%}%> <!-- Minimum password length -->
</div> <%- include("partials/admin-setting-number.ejs", {setting: {name:'USER_MINIMUM_PASSWORD_LENGTH', name_pretty: 'Minimum Password Length', value:
</div> settings.USER_MINIMUM_PASSWORD_LENGTH}}) %>
</div>
<div class="settings-row"> <div id="content-delivery" class="category-page hidden">
<div class="label">Hide Login</div> <!-- RSS -->
<div class="button-group"> <%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'CD_RSS', name_pretty: 'RSS Feed', enabled: settings.CD_RSS}}) %>
<div class="spinner hidden"> <!-- ActivityPub -->
<div>↺</div> <%- include("partials/admin-setting-toggle.ejs", {setting: {name: 'CD_AP', name_pretty: 'Activity Pub Feed', enabled: settings.CD_AP}}) %>
</div> <!-- TODO: Add these option automatically to footer on active -->
<% if (!settings.HIDE_LOGIN ) { %> </div>
<button id="hidelogin_toggle" onclick="toggleState('HIDE_LOGIN', true, this.id)" class="bad"><div>Disabled</div></button>
<% } else { %> <div id="groups" class="category-page hidden">TODO</div>
<button id="hidelogin_toggle" onclick="toggleState('HIDE_LOGIN', false, this.id)" class="good"><div>Enabled</div></button> <div id="yab-master" class="category-page hidden">
<%}%> <!-- Website title -->
</div> <%- include("partials/admin-setting-text.ejs", {setting: {name: 'WEBSITE_NAME', name_pretty: 'Website Title', value: settings.WEBSITE_NAME}}) %>
</div> <!-- 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> </div>
</div> </div>

View File

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

View File

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

View File

@ -0,0 +1,13 @@
<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>