removed remainder of old personal dashboard code. fixed big lighthouse issues.

This commit is contained in:
jude 2022-04-07 21:41:24 +01:00
parent 85d27c5bba
commit 0f05018cab
12 changed files with 593 additions and 389 deletions

View File

@ -24,10 +24,11 @@ CREATE TABLE reminder_template (
`embed_author` VARCHAR(256) NOT NULL DEFAULT '',
`embed_author_url` VARCHAR(512),
`embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0,
`embed_fields` JSON,
PRIMARY KEY (id),
FOREIGN KEY (`guild_id`) REFERENCES channels (`id`) ON DELETE CASCADE
FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE
);
ALTER TABLE reminders ADD COLUMN embed_fields JSON;

View File

@ -11,7 +11,8 @@ use std::{collections::HashMap, env};
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use rocket::{
fs::FileServer,
serde::json::{json, Json, Value as JsonValue},
serde::json::{json, Value as JsonValue},
shield::Shield,
tokio::sync::broadcast::Sender,
};
use rocket_dyn_templates::Template;
@ -119,12 +120,15 @@ pub async fn initialize(
.mount(
"/dashboard",
routes![
routes::dashboard::dashboard,
routes::dashboard::dashboard_home,
routes::dashboard::user::get_user_info,
routes::dashboard::user::update_user_info,
routes::dashboard::user::get_user_guilds,
routes::dashboard::guild::get_guild_channels,
routes::dashboard::guild::get_guild_roles,
routes::dashboard::guild::get_reminder_templates,
routes::dashboard::guild::create_reminder_template,
routes::dashboard::guild::create_reminder,
routes::dashboard::guild::get_reminders,
routes::dashboard::guild::edit_reminder,

View File

@ -24,8 +24,8 @@ use crate::{
MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL,
},
routes::dashboard::{
create_database_channel, generate_uid, name_default, DeleteReminder, PatchReminder,
Reminder,
create_database_channel, generate_uid, name_default, template_name_default, DeleteReminder,
PatchReminder, Reminder, ReminderTemplate,
},
};
@ -131,6 +131,135 @@ pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Conte
}
}
#[get("/api/guild/<id>/templates")]
pub async fn get_reminder_templates(
id: u64,
cookies: &CookieJar<'_>,
ctx: &State<Context>,
pool: &State<Pool<MySql>>,
) -> JsonValue {
check_authorization!(cookies, ctx.inner(), id);
match sqlx::query_as_unchecked!(
ReminderTemplate,
"SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
id
)
.fetch_all(pool.inner())
.await
{
Ok(templates) => {
json!(templates)
}
Err(e) => {
warn!("Could not fetch templates from {}: {:?}", id, e);
json!({"error": "Could not get templates"})
}
}
}
#[post("/api/guild/<id>/templates", data = "<reminder_template>")]
pub async fn create_reminder_template(
id: u64,
reminder_template: Json<ReminderTemplate>,
cookies: &CookieJar<'_>,
ctx: &State<Context>,
pool: &State<Pool<MySql>>,
) -> JsonValue {
check_authorization!(cookies, ctx.inner(), id);
// validate lengths
check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
if let Some(fields) = &reminder_template.embed_fields {
for field in &fields.0 {
check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
}
}
check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username);
check_length_opt!(
MAX_URL_LENGTH,
reminder_template.embed_footer_url,
reminder_template.embed_thumbnail_url,
reminder_template.embed_author_url,
reminder_template.embed_image_url,
reminder_template.avatar
);
// validate urls
check_url_opt!(
reminder_template.embed_footer_url,
reminder_template.embed_thumbnail_url,
reminder_template.embed_author_url,
reminder_template.embed_image_url,
reminder_template.avatar
);
let name = if reminder_template.name.is_empty() {
template_name_default()
} else {
reminder_template.name.clone()
};
match sqlx::query!(
"INSERT INTO reminder_template
(guild_id,
name,
attachment,
attachment_name,
avatar,
content,
embed_author,
embed_author_url,
embed_color,
embed_description,
embed_footer,
embed_footer_url,
embed_image_url,
embed_thumbnail_url,
embed_title,
embed_fields,
tts,
username
) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
id, name,
reminder_template.attachment,
reminder_template.attachment_name,
reminder_template.avatar,
reminder_template.content,
reminder_template.embed_author,
reminder_template.embed_author_url,
reminder_template.embed_color,
reminder_template.embed_description,
reminder_template.embed_footer,
reminder_template.embed_footer_url,
reminder_template.embed_image_url,
reminder_template.embed_thumbnail_url,
reminder_template.embed_title,
reminder_template.embed_fields,
reminder_template.tts,
reminder_template.username,
)
.fetch_all(pool.inner())
.await
{
Ok(_) => {
json!({})
}
Err(e) => {
warn!("Could not fetch templates from {}: {:?}", id, e);
json!({"error": "Could not get templates"})
}
}
}
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
pub async fn create_reminder(
id: u64,
@ -550,9 +679,8 @@ pub async fn edit_reminder(
}
}
#[delete("/api/guild/<id>/reminders", data = "<reminder>")]
#[delete("/api/guild/<_>/reminders", data = "<reminder>")]
pub async fn delete_reminder(
id: u64,
reminder: Json<DeleteReminder>,
pool: &State<Pool<MySql>>,
) -> JsonValue {

View File

@ -22,10 +22,44 @@ fn name_default() -> String {
"Reminder".to_string()
}
fn template_name_default() -> String {
"Template".to_string()
}
fn channel_default() -> u64 {
0
}
fn id_default() -> u32 {
0
}
#[derive(Serialize, Deserialize)]
pub struct ReminderTemplate {
#[serde(default = "id_default")]
id: u32,
#[serde(default = "id_default")]
guild_id: u32,
#[serde(default = "template_name_default")]
name: String,
attachment: Option<Vec<u8>>,
attachment_name: Option<String>,
avatar: Option<String>,
content: String,
embed_author: String,
embed_author_url: Option<String>,
embed_color: u32,
embed_description: String,
embed_footer: String,
embed_footer_url: Option<String>,
embed_image_url: Option<String>,
embed_thumbnail_url: Option<String>,
embed_title: String,
embed_fields: Option<Json<Vec<EmbedField>>>,
tts: bool,
username: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct EmbedField {
title: String,
@ -241,3 +275,13 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirec
Err(Redirect::to("/login/discord"))
}
}
#[get("/<_>")]
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
if cookies.get_private("userid").is_some() {
let map: HashMap<&str, String> = HashMap::new();
Ok(Template::render("dashboard", &map))
} else {
Err(Redirect::to("/login/discord"))
}
}

View File

@ -3,52 +3,61 @@
font-style: italic;
font-weight: 300;
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18E.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDc.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 600;
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18E.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdr.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7g.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdr.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdr.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgo6eA.ttf) format('truetype');
font-display: swap;
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvTtw.ttf) format('truetype');
font-display: swap;
}

View File

@ -2,6 +2,10 @@
font-family: "Ubuntu Bold", "Ubuntu", sans-serif;
}
button {
font-weight: 700;
}
/* override styles for when the div is collapsed */
div.reminderContent.is-collapsed .column.discord-frame {
display: none;
@ -55,6 +59,11 @@ div.reminderContent.is-collapsed button.hide-box i {
/* END */
/* dashboard styles */
button.inline-btn {
height: 100%;
padding: 5px;
}
button.change-color {
position: absolute;
left: calc(-1rem - 40px);
@ -88,7 +97,7 @@ div.interval-group > button {
}
/* Interval inputs */
div.interval-group > .interval-group-left > input {
div.interval-group > .interval-group-left input {
-webkit-appearance: none;
border-style: none;
background-color: #eee;
@ -96,11 +105,11 @@ div.interval-group > .interval-group-left > input {
font-family: monospace;
}
div.interval-group > .interval-group-left > input.w2 {
div.interval-group > .interval-group-left input.w2 {
width: 3ch;
}
div.interval-group > .interval-group-left > input.w3 {
div.interval-group > .interval-group-left input.w3 {
width: 6ch;
}
@ -153,6 +162,15 @@ span.patreon-color {
color: #f96854;
}
p.pageTitle {
margin-left: 12px;
}
#welcome > div {
height: 100%;
padding-top: 30vh;
}
div#pageNavbar {
background-color: #363636;
}
@ -188,6 +206,17 @@ div.dashboard-sidebar {
padding-right: 0;
}
div.dashboard-sidebar:not(.mobile-sidebar) {
display: flex;
flex-direction: column;
}
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
position: fixed;
bottom: 0;
width: 226px;
}
div.mobile-sidebar {
z-index: 100;
min-height: 100vh;
@ -197,10 +226,24 @@ div.mobile-sidebar {
flex-direction: column;
}
#expandAll {
width: 60px;
}
div.mobile-sidebar .aside-footer {
margin-top: auto;
}
div.mobile-sidebar.is-active {
display: flex;
}
aside.menu {
display: flex;
flex-direction: column;
flex-grow: 1;
}
div.dashboard-frame {
min-height: 100vh;
margin-bottom: 0 !important;
@ -475,3 +518,40 @@ textarea, input {
height: 16px;
}
}
/* loader */
#loader {
position: fixed;
background-color: rgba(255, 255, 255, 0.8);
width: 100vw;
z-index: 999;
}
#loader .title {
font-size: 6rem;
}
/* END */
/* other stuff */
.half-rem {
width: 0.5rem;
}
.pad-left {
width: 12px;
}
#dead {
display: none;
}
.colorpicker-container {
display: flex;
justify-content: center;
}
.create-reminder {
margin: 0 12px 12px 12px;
}

View File

@ -6,9 +6,19 @@ const $colorPickerInput = $colorPickerModal.querySelector("input");
const $deleteReminderBtn = document.querySelector("#delete-reminder-confirm");
const $reminderTemplate = document.querySelector("template#guildReminder");
const $embedFieldTemplate = document.querySelector("template#embedFieldTemplate");
const $createReminder = document.querySelector("#reminderCreator");
const $createReminderBtn = $createReminder.querySelector("button#createReminder");
const $createTemplateBtn = $createReminder.querySelector("button#createTemplate");
const $loadTemplateBtn = document.querySelector("button#load-template");
const $templateSelect = document.querySelector("select#templateSelect");
let channels;
let roles;
let channels = [];
let roles = [];
let templates = {};
function guildId() {
return document.querySelector(".guildList a.is-active").dataset["guild"];
}
function colorToInt(r, g, b) {
return (r << 16) + (g << 8) + b;
@ -77,6 +87,30 @@ function fetch_roles(guild_id) {
});
}
function fetch_templates(guild_id) {
fetch(`/dashboard/api/guild/${guild_id}/templates`)
.then((response) => response.json())
.then((data) => {
if (data.error) {
show_error(data.error);
} else {
templates = {};
const select = document.querySelector("#templateSelect");
select.innerHTML = "";
for (let template of data) {
templates[template["id"]] = template;
let option = document.createElement("option");
option.value = template["id"];
option.textContent = template["name"];
select.appendChild(option);
}
}
});
}
async function fetch_channels(guild_id) {
const event = new Event("channelsLoading");
document.dispatchEvent(event);
@ -121,7 +155,7 @@ async function fetch_reminders(guild_id) {
newFrame.querySelector(".reminderContent").dataset["uid"] =
reminder["uid"];
deserialize_reminder(reminder, newFrame);
deserialize_reminder(reminder, newFrame, "load");
$reminderBox.appendChild(newFrame);
@ -137,8 +171,21 @@ async function fetch_reminders(guild_id) {
});
}
async function serialize_reminder(node) {
let interval = get_interval(node);
async function serialize_reminder(node, mode) {
let interval, utc_time;
if (mode !== "template") {
interval = get_interval(node);
utc_time = luxon.DateTime.fromISO(
node.querySelector('input[name="time"]').value
).setZone("UTC");
if (utc_time.invalid) {
return { error: "Time provided invalid." };
} else {
utc_time = utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss");
}
}
let rgb_color = window.getComputedStyle(
node.querySelector("div.discord-embed")
@ -146,21 +193,13 @@ async function serialize_reminder(node) {
let rgb = rgb_color.match(/\d+/g);
let color = colorToInt(parseInt(rgb[0]), parseInt(rgb[1]), parseInt(rgb[2]));
let utc_time = luxon.DateTime.fromISO(
node.querySelector('input[name="time"]').value
).setZone("UTC");
if (utc_time.invalid) {
return { error: "Time provided invalid." };
}
let fields = [
...node.querySelectorAll("div.embed-multifield-box div.embed-field-box"),
]
.map((el) => {
return {
title: el.querySelector("textarea#embedFieldTitle").value,
value: el.querySelector("textarea#embedFieldValue").value,
title: el.querySelector("textarea.discord-field-title").value,
value: el.querySelector("textarea.discord-field-value").value,
inline: el.dataset["inlined"] === "1",
};
})
@ -181,13 +220,21 @@ async function serialize_reminder(node) {
attachment_name = file.name;
}
const reminderContent = node.closest(".reminderContent");
let uid = "";
if (mode === "edit") {
uid = node.closest(".reminderContent").dataset["uid"];
}
let enabled = null;
if (mode === "create") {
enabled = true;
}
return {
// if we're creating a reminder, ignore this field
uid: reminderContent !== null ? reminderContent.dataset["uid"] : "",
uid: uid,
// if we're editing a reminder, ignore this field
enabled: reminderContent !== null ? null : true,
enabled: enabled,
restartable: false,
attachment: attachment,
attachment_name: attachment_name,
@ -207,17 +254,17 @@ async function serialize_reminder(node) {
embed_title: node.querySelector('textarea[name="embed_title"]').value,
embed_fields: fields,
expires: null,
interval_seconds: interval.seconds,
interval_months: interval.months,
interval_seconds: mode !== "template" ? interval.seconds : null,
interval_months: mode !== "template" ? interval.months : null,
name: node.querySelector('input[name="name"]').value,
pin: node.querySelector('input[name="pin"]').checked,
tts: node.querySelector('input[name="tts"]').checked,
username: node.querySelector('input[name="username"]').value,
utc_time: utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"),
utc_time: utc_time,
};
}
function deserialize_reminder(reminder, frame) {
function deserialize_reminder(reminder, frame, mode) {
// populate channels
set_channels(frame.querySelector("select.channel-selector"));
@ -256,16 +303,18 @@ function deserialize_reminder(reminder, frame) {
.insertBefore(embed_field, lastChild);
}
if (reminder["interval_seconds"] !== null) update_interval(frame);
if (mode !== "template") {
if (reminder["interval_seconds"]) update_interval(frame);
let $enableBtn = frame.querySelector(".disable-enable");
$enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable";
let timeInput = frame.querySelector('input[name="time"]');
let localTime = luxon.DateTime.fromISO(reminder["utc_time"], { zone: "UTC" }).setZone(
timezone
);
let localTime = luxon.DateTime.fromISO(reminder["utc_time"], {
zone: "UTC",
}).setZone(timezone);
timeInput.value = localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss");
}
}
document.addEventListener("guildSwitched", async (e) => {
@ -276,11 +325,11 @@ document.addEventListener("guildSwitched", async (e) => {
);
switch_pane($anchor.dataset["pane"]);
reset_guild_pane();
$anchor.classList.add("is-active");
reset_guild_pane();
fetch_roles(e.detail.guild_id);
fetch_templates(e.detail.guild_id);
await fetch_channels(e.detail.guild_id);
fetch_reminders(e.detail.guild_id);
@ -303,7 +352,7 @@ document.addEventListener("channelsLoaded", () => {
});
document.addEventListener("remindersLoaded", (event) => {
const guild = document.querySelector(".guildList a.is-active").dataset["guild"];
const guild = guildId();
for (let reminder of event.detail) {
let node = reminder.node;
@ -351,13 +400,13 @@ document.addEventListener("remindersLoaded", (event) => {
"fas fa-spinner fa-spin",
];
let reminder = await serialize_reminder(node);
let reminder = await serialize_reminder(node, "edit");
if (reminder.error) {
show_error(reminder.error);
return;
}
let guild = document.querySelector(".guildList a.is-active").dataset["guild"];
let guild = guildId();
fetch(`/dashboard/api/guild/${guild}/reminders`, {
method: "PATCH",
@ -381,7 +430,7 @@ document.addEventListener("remindersLoaded", (event) => {
});
$deleteReminderBtn.addEventListener("click", () => {
let guild = document.querySelector(".guildList a.is-active").dataset["guild"];
let guild = guildId();
fetch(`/dashboard/api/guild/${guild}/reminders`, {
method: "DELETE",
@ -422,7 +471,7 @@ $colorPickerModal.querySelector("button.is-success").addEventListener("click", (
$colorPickerModal.classList.remove("is-active");
});
document.querySelectorAll("a.show-modal").forEach((element) => {
document.querySelectorAll(".show-modal").forEach((element) => {
element.addEventListener("click", (e) => {
e.preventDefault();
document.getElementById(element.dataset["modal"]).classList.toggle("is-active");
@ -480,10 +529,15 @@ document.addEventListener("DOMContentLoaded", () => {
);
$anchor.dataset["guild"] = guild.id;
$anchor.dataset["name"] = guild.name;
$anchor.href = `/dashboard/${guild.id}?name=${guild.name}`;
$anchor.addEventListener("click", async (e) => {
e.preventDefault();
window.history.pushState(
{},
"",
`/dashboard/${guild.id}?name=${guild.name}`
);
const event = new CustomEvent("guildSwitched", {
detail: {
guild_name: guild.name,
@ -497,6 +551,21 @@ document.addEventListener("DOMContentLoaded", () => {
element.append($clone);
});
}
const matches = window.location.href.match(/dashboard\/(\d+)/);
if (matches) {
let id = matches[1];
let name =
new URLSearchParams(window.location.search).get("name") || id;
const event = new CustomEvent("guildSwitched", {
detail: {
guild_name: name,
guild_id: id,
},
});
document.dispatchEvent(event);
}
}
});
@ -530,34 +599,18 @@ function has_source(string) {
}
}
let $createReminder = document.querySelector("#reminderCreator");
let $createBtn = $createReminder.querySelector("button#createReminder");
$createReminderBtn.addEventListener("click", async () => {
$createReminderBtn.querySelector("span.icon > i").classList = [
"fas fa-spinner fa-spin",
];
$createBtn.addEventListener("click", async () => {
$createBtn.querySelector("span.icon > i").classList = ["fas fa-spinner fa-spin"];
let attachment = null;
let attachment_name = null;
if ($createReminder.querySelector('input[name="attachment"]').files.length > 0) {
let file = $createReminder.querySelector('input[name="attachment"]').files[0];
attachment = await new Promise((resolve) => {
let fileReader = new FileReader();
fileReader.onload = (e) => resolve(fileReader.result);
fileReader.readAsDataURL(file);
});
attachment = attachment.split(",")[1];
attachment_name = file.name;
}
let reminder = await serialize_reminder($createReminder);
let reminder = await serialize_reminder($createReminder, "create");
if (reminder.error) {
show_error(reminder.error);
return;
}
let guild = document.querySelector(".guildList a.is-active").dataset["guild"];
let guild = guildId();
fetch(`/dashboard/api/guild/${guild}/reminders`, {
method: "POST",
@ -570,13 +623,17 @@ $createBtn.addEventListener("click", async () => {
.then((data) => {
if (data.error) {
show_error(data.error);
$createReminderBtn.querySelector("span.icon > i").classList = [
"fas fa-sparkles",
];
} else {
const $reminderBox = document.querySelector("div#guildReminders");
let newFrame = $reminderTemplate.content.cloneNode(true);
newFrame.querySelector(".reminderContent").dataset["uid"] = data["uid"];
deserialize_reminder(data, newFrame);
deserialize_reminder(data, newFrame, "load");
$reminderBox.appendChild(newFrame);
@ -587,16 +644,62 @@ $createBtn.addEventListener("click", async () => {
detail: [data],
})
);
}
$createBtn.querySelector("span.icon > i").classList = ["fas fa-check"];
$createReminderBtn.querySelector("span.icon > i").classList = [
"fas fa-check",
];
window.setTimeout(() => {
$createBtn.querySelector("span.icon > i").classList = ["fas fa-sparkles"];
$createReminderBtn.querySelector("span.icon > i").classList = [
"fas fa-sparkles",
];
}, 1500);
}
});
});
$createTemplateBtn.addEventListener("click", async () => {
let reminder = await serialize_reminder($createReminder, "template");
let guild = guildId();
fetch(`/dashboard/api/guild/${guild}/templates`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reminder),
})
.then((response) => response.json())
.then((data) => {
if (data.error) {
show_error(data.error);
$createTemplateBtn.querySelector("span.icon > i").classList = [
"fas fa-file-spreadsheet",
];
} else {
fetch_templates(guildId());
$createTemplateBtn.querySelector("span.icon > i").classList = [
"fas fa-check",
];
window.setTimeout(() => {
$createTemplateBtn.querySelector("span.icon > i").classList = [
"fas fa-file-spreadsheet",
];
}, 1500);
}
});
});
$loadTemplateBtn.addEventListener("click", (ev) => {
deserialize_reminder(
templates[parseInt($templateSelect.value)],
$createReminder,
"template"
);
});
document.querySelectorAll("textarea.autoresize").forEach((element) => {
element.addEventListener("input", () => {
element.style.height = "";

View File

@ -6,6 +6,7 @@
<meta charset="UTF-8">
<meta name="yandex-verification" content="bb77b8681eb64a90"/>
<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'">
<!-- favicon -->
<link rel="apple-touch-icon" sizes="180x180"
@ -27,10 +28,7 @@
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/dtsel.css">
<script src="/static/js/iro.js"></script>
<script src="/static/js/dtsel.js"></script>
<script src="/static/js/luxon.min.js"></script>
</head>
<body>
<nav class="navbar is-spaced is-size-4 is-hidden-desktop dashboard-navbar" role="navigation"
@ -54,10 +52,10 @@
</div>
</nav>
<div id="loader" class="is-hidden hero is-fullheight" style="position: fixed; background-color: rgba(255, 255, 255, 0.8); width: 100vw; z-index: 999;">
<div id="loader" class="is-hidden hero is-fullheight">
<div class="hero-body">
<div class="container has-text-centered">
<p class="title" style="font-size: 6rem; color: #8fb677">
<p class="title">
<i class="fas fa-cog fa-spin"></i>
</p>
<p class="subtitle">
@ -68,7 +66,7 @@
</div>
<!-- dead image used to check which other images are dead -->
<img style="display: none;" src="" id="dead">
<img src="" id="dead">
<div class="notification is-danger flash-message" id="errors">
<span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span>
@ -100,7 +98,7 @@
<button class="delete close-modal" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div style="display: flex; justify-content: center">
<div class="colorpicker-container">
<div id="colorpicker"></div>
</div>
<input class="input" id="colorInput">
@ -140,6 +138,33 @@
<button class="modal-close is-large close-modal" aria-label="close"></button>
</div>
<div class="modal" id="chooseTemplateModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<label class="modal-card-title" for="urlInput">Load Template</label>
<button class="delete close-modal" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="control has-icons-left">
<div class="select is-fullwidth">
<select id="templateSelect">
</select>
</div>
<div class="icon is-small is-left">
<i class="fas fa-file-spreadsheet"></i>
</div>
</div>
<br>
<div class="has-text-centered">
<button class="button is-success close-modal" id="load-template">Load Template</button>
<button class="button is-danger" id="delete-template">Delete</button>
</div>
</section>
</div>
<button class="modal-close is-large close-modal" aria-label="close"></button>
</div>
<div class="modal" id="dataManagerModal">
<div class="modal-background"></div>
<div class="modal-card">
@ -179,7 +204,7 @@
</div>
<div class="columns is-gapless dashboard-frame">
<div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch" style="display: flex; flex-direction: column;">
<div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch">
<a href="/">
<div class="brand">
<img src="/static/img/logo_flat.webp" alt="Reminder bot logo"
@ -192,14 +217,14 @@
d="M0,192L60,170.7C120,149,240,107,360,96C480,85,600,107,720,138.7C840,171,960,213,1080,197.3C1200,181,1320,107,1380,69.3L1440,32L1440,0L1380,0C1320,0,1200,0,1080,0C960,0,840,0,720,0C600,0,480,0,360,0C240,0,120,0,60,0L0,0Z"></path>
</g>
</svg>
<aside class="menu" style="display: flex; flex-direction: column; flex-grow: 1;">
<aside class="menu">
<p class="menu-label">
Servers
</p>
<ul class="menu-list guildList">
</ul>
<div class="aside-footer" style="position: fixed; bottom: 0; width: 226px;">
<div class="aside-footer">
<p class="menu-label">
Settings
</p>
@ -230,14 +255,14 @@
d="M0,192L60,170.7C120,149,240,107,360,96C480,85,600,107,720,138.7C840,171,960,213,1080,197.3C1200,181,1320,107,1380,69.3L1440,32L1440,0L1380,0C1320,0,1200,0,1080,0C960,0,840,0,720,0C600,0,480,0,360,0C240,0,120,0,60,0L0,0Z"></path>
</g>
</svg>
<aside class="menu" style="display: flex; flex-direction: column; flex-grow: 1;">
<aside class="menu">
<p class="menu-label">
Servers
</p>
<ul class="menu-list guildList">
</ul>
<div class="aside-footer" style="margin-top: auto;">
<div class="aside-footer">
<p class="menu-label">
Settings
</p>
@ -257,17 +282,14 @@
<!-- main content -->
<div class="column is-main-content">
<p class="title pageTitle" style="margin-left: 12px;"></p>
<p class="title pageTitle"></p>
<section id="welcome">
<div class="has-text-centered" style="height: 100%; padding-top: 30vh;">
<div class="has-text-centered">
<p class="title">Welcome!</p>
<p class="subtitle is-hidden-touch">Select an option from the side to get started</p>
<p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p>
</div>
</section>
<section id="personal" class="is-hidden">
{% include "reminder_dashboard/reminder_dashboard_personal" %}
</section>
<section id="guild" class="is-hidden">
{% include "reminder_dashboard/reminder_dashboard" %}
</section>
@ -294,20 +316,26 @@
<template id="embedFieldTemplate">
<div data-inlined="1" class="embed-field-box">
<label class="is-sr-only" for="embedFieldTitle">Field Title</label>
<div style="display: flex;">
<div class="is-flex">
<label>
<span class="is-sr-only">Field Title</span>
<textarea class="discord-field-title field-input message-input autoresize"
placeholder="Field Title..." rows="1"
maxlength="256" id="embedFieldTitle" name="embed_field_title[]"></textarea>
<button class="button is-small inline-btn" style="height: 100%; padding: 5px;"><i class="fas fa-arrows-h"></i></button>
maxlength="256" name="embed_field_title[]"></textarea>
</label>
<button class="button is-small inline-btn">
<span class="is-sr-only">Toggle field inline</span><i class="fas fa-arrows-h"></i>
</button>
</div>
<label class="is-sr-only" for="embedFieldValue">Field Value</label>
<label>
<span class="is-sr-only">Field Value</span>
<textarea
class="discord-field-value field-input message-input autoresize"
placeholder="Field Value..."
maxlength="1024" id="embedFieldValue" name="embed_field_value[]"
maxlength="1024" name="embed_field_value[]"
rows="1"></textarea>
</label>
</div>
</template>
@ -323,13 +351,12 @@
{% include "reminder_dashboard/guild_reminder" %}
</template>
<template id="personalReminder">
{% include "reminder_dashboard/personal_reminder" %}
</template>
<script src="/static/js/iro.js"></script>
<script src="/static/js/dtsel.js"></script>
<script src="/static/js/interval.js"></script>
<script src="/static/js/timezone.js"></script>
<script src="/static/js/main.js"></script>
<script src="/static/js/timezone.js" defer></script>
<script src="/static/js/main.js" defer></script>
</body>
</html>

View File

@ -4,33 +4,33 @@
<figure class="media-left">
<p class="image is-32x32 customizable">
<a>
<img class="is-rounded discord-avatar" src="/static/img/bg.webp">
<img class="is-rounded discord-avatar" src="/static/img/bg.webp" alt="Image for discord avatar">
</a>
</p>
</figure>
<div class="media-content">
<div class="content">
<div class="discord-message-header">
<label class="is-sr-only" for="reminderUsername">Username Override</label>
<label class="is-sr-only">Username Override</label>
<input class="discord-username message-input" placeholder="Username Override"
maxlength="32" id="reminderUsername" name="username">
maxlength="32" name="username">
</div>
<label class="is-sr-only" for="messageContent">Message</label>
<label class="is-sr-only">Message</label>
<textarea class="message-input autoresize discord-content"
placeholder="Message Content..."
maxlength="2000" id="messageContent" name="content" rows="1"></textarea>
maxlength="2000" name="content" rows="1"></textarea>
<div class="discord-embed">
<div class="embed-body">
<button class="change-color button is-rounded is-small">
<i class="fas fa-eye-dropper"></i>
<span class="is-sr-only">Choose embed color</span><i class="fas fa-eye-dropper"></i>
</button>
<div class="a">
<div class="embed-author-box">
<div class="a">
<p class="image is-24x24 customizable">
<a>
<img class="is-rounded embed_author_url" src="/static/img/bg.webp">
<img class="is-rounded embed_author_url" src="/static/img/bg.webp" alt="Image for embed author">
</a>
</p>
</div>
@ -40,38 +40,40 @@
<textarea
class="discord-embed-author message-input autoresize"
placeholder="Embed Author..." rows="1" maxlength="256"
id="embedAuthor" name="embed_author"></textarea>
name="embed_author"></textarea>
</div>
</div>
<label class="is-sr-only" for="embedTitle">Embed Title</label>
<textarea class="discord-title message-input autoresize"
placeholder="Embed Title..."
maxlength="256" id="embedTitle" rows="1"
maxlength="256" rows="1"
name="embed_title"></textarea>
<br>
<label class="is-sr-only" for="embedDescription">Embed Description</label>
<textarea class="discord-description message-input autoresize "
placeholder="Embed Description..."
maxlength="4096" id="embedDescription" name="embed_description"
maxlength="4096" name="embed_description"
rows="1"></textarea>
<br>
<div class="embed-multifield-box">
<div data-inlined="1" class="embed-field-box">
<label class="is-sr-only" for="embedFieldTitle">Field Title</label>
<div style="display: flex;">
<div class="is-flex">
<textarea class="discord-field-title field-input message-input autoresize"
placeholder="Field Title..." rows="1"
maxlength="256" id="embedFieldTitle" name="embed_field_title[]"></textarea>
<button class="button is-small inline-btn" style="height: 100%; padding: 5px;"><i class="fas fa-arrows-h"></i></button>
maxlength="256" name="embed_field_title[]"></textarea>
<button class="button is-small inline-btn">
<span class="is-sr-only">Toggle field inline</span><i class="fas fa-arrows-h"></i>
</button>
</div>
<label class="is-sr-only" for="embedFieldValue">Field Value</label>
<textarea
class="discord-field-value field-input message-input autoresize "
placeholder="Field Value..."
maxlength="1024" id="embedFieldValue" name="embed_field_value[]"
maxlength="1024" name="embed_field_value[]"
rows="1"></textarea>
</div>
</div>
@ -102,7 +104,7 @@
<label class="is-sr-only" for="embedFooter">Embed Footer text</label>
<textarea class="discord-embed-footer message-input autoresize "
placeholder="Embed Footer..."
maxlength="2048" id="embedFooter" name="embed_footer" rows="1"></textarea>
maxlength="2048" name="embed_footer" rows="1"></textarea>
</div>
</div>
</div>
@ -121,7 +123,7 @@
</div>
<div class="column is-narrow">
<button class="button is-rounded hide-box">
<i class="fas fa-chevron-down"></i>
<span class="is-sr-only">Hide reminder</span><i class="fas fa-chevron-down"></i>
</button>
</div>
</div>
@ -132,20 +134,21 @@
</div>
<div class="control has-icons-left">
<div class="select">
<select id="channelOption" name="channel" class="channel-selector">
<select name="channel" class="channel-selector">
</select>
</div>
<div class="icon is-small is-left">
<i class="fas fa-hashtag"></i>
</div>
</div>
</div>
<div class="collapses">
<div class="field">
<label class="label">Time</label>
<div class="control">
<label class="label">
Time
<input class="input" type="datetime-local" step="1" name="time">
</label>
</div>
</div>
@ -154,13 +157,28 @@
<div class="control intervalSelector">
<div class="input interval-group">
<div class="interval-group-left">
<input class="w2" type="text" pattern="\d*" name="interval_months" maxlength="2" placeholder=""> <span style="width: 0.5rem"></span> months, <span style="width: 0.5rem"></span>
<input class="w3" type="text" pattern="\d*" name="interval_days" maxlength="4" placeholder=""> <span style="width: 0.5rem"></span> days, <span style="width: 0.5rem"></span>
<label>
<span class="is-sr-only">Interval months</span>
<input class="w2" type="text" pattern="\d*" name="interval_months" maxlength="2" placeholder=""> <span class="half-rem"></span> months, <span class="half-rem"></span>
</label>
<label>
<span class="is-sr-only">Interval days</span>
<input class="w3" type="text" pattern="\d*" name="interval_days" maxlength="4" placeholder=""> <span class="half-rem"></span> days, <span class="half-rem"></span>
</label>
<label>
<span class="is-sr-only">Interval hours</span>
<input class="w2" type="text" pattern="\d*" name="interval_hours" maxlength="2" placeholder="HH">:
</label>
<label>
<span class="is-sr-only">Interval minutes</span>
<input class="w2" type="text" pattern="\d*" name="interval_minutes" maxlength="2" placeholder="MM">:
</label>
<label>
<span class="is-sr-only">Interval seconds</span>
<input class="w2" type="text" pattern="\d*" name="interval_seconds" maxlength="2" placeholder="SS">
</label>
</div>
<button class="clear"><span class="icon"><i class="fas fa-trash"></i></span></button>
<button class="clear"><span class="is-sr-only">Clear interval</span><span class="icon"><i class="fas fa-trash"></i></span></button>
</div>
</div>
</div>
@ -193,13 +211,13 @@
</div>
</div>
<span style="width: 12px;"></span>
<span class="pad-left"></span>
{% if creating %}
<button class="button is-outlined">
<button class="button is-outlined show-modal" data-modal="chooseTemplateModal">
Load Template
</button>
<button class="button is-success is-outlined" id="createTemplate">
Create Template
<span>Create Template</span> <span class="icon"><i class="fas fa-file-spreadsheet"></i></span>
</button>
<button class="button is-success" id="createReminder">
<span>Create Reminder</span> <span class="icon"><i class="fas fa-sparkles"></i></span>
@ -209,6 +227,7 @@
<span>Save</span> <span class="icon"><i class="fas fa-save"></i></span>
</button>
<button class="button is-warning disable-enable">
<span class="is-sr-only">Text content filled by CSS</span>
</button>
<button class="button is-danger delete-reminder">
Delete

View File

@ -1,186 +0,0 @@
<div class="discord-frame">
<article class="media">
<figure class="media-left">
<p class="image is-32x32">
<img class="is-rounded" src="/static/img/icon.png" alt="reminder bot icon">
</p>
</figure>
<div class="media-content">
<div class="content">
<div class="is-hidden-touch">
<div class="discord-message-header">
Reminder Bot -
<label class="is-sr-only" for="reminderDate">Reminder Date</label>
<input class="time-input date" placeholder="YYYY/MM/DD"
id="reminderDate" name="date">
<label class="is-sr-only" for="reminderTime">Reminder Time</label>
<input class="time-input time" placeholder="HH:MM:SS"
id="reminderTime" name="time">
</div>
</div>
<div class="is-hidden-desktop">
<label class="is-sr-only" for="reminderDate">Reminder Date</label>
<input class="time-input date" placeholder="YYYY/MM/DD"
id="reminderDate" name="date">
<label class="is-sr-only" for="reminderTime">Reminder Time</label>
<input class="time-input time" placeholder="HH:MM:SS"
id="reminderTime" name="time">
<div class="discord-message-header">
Reminder Bot
</div>
</div>
<label class="is-sr-only" for="messageContent">Message</label>
<textarea class="message-input autoresize discord-content preview-mode"
placeholder="Message Content..."
maxlength="2000" id="messageContent" name="content" rows="1"></textarea>
<div class="discord-embed">
<div class="embed-body">
<div class="a">
<div class="embed-author-box">
<div class="a">
<p class="image is-24x24 customizable">
<a>
<img class="is-rounded preview-mode" src="">
</a>
</p>
</div>
<div class="b">
<label class="is-sr-only" for="embedAuthor">Embed Author</label>
<textarea
class="discord-embed-author message-input preview-mode autoresize"
placeholder="Embed Author..." rows="1" maxlength="256"
id="embedAuthor" name="embed_author"></textarea>
</div>
</div>
<label class="is-sr-only" for="embedTitle">Embed Title</label>
<textarea class="discord-title message-input preview-mode autoresize"
placeholder="Embed Title..."
maxlength="256" id="embedTitle" rows="1"
name="embed_title"></textarea>
<br>
<label class="is-sr-only" for="embedDescription">Embed Description</label>
<textarea class="discord-description message-input autoresize preview-mode"
placeholder="Embed Description..."
maxlength="2048" id="embedDescription" name="embed_description"
rows="1"></textarea>
<br>
<div class="embed-multifield-box">
<div class="embed-field-box">
<label class="is-sr-only" for="embedFieldTitle">Field Title</label>
<textarea
class="discord-field-title field-input message-input autoresize preview-mode"
placeholder="Field Title..." rows="1"
maxlength="256" id="embedFieldTitle"
name="embed_field_title[]"></textarea>
<label class="is-sr-only" for="embedFieldValue">Field Value</label>
<textarea
class="discord-field-value field-input message-input autoresize preview-mode"
placeholder="Field Value..."
maxlength="1024" id="embedFieldValue" name="embed_field_value[]"
rows="1"></textarea>
</div>
</div>
</div>
<div class="b">
<p class="image thumbnail customizable">
<a>
<img class="preview-mode" src="">
</a>
</p>
</div>
</div>
<p class="image is-400x300 customizable">
<a>
<img class="preview-mode" src="">
</a>
</p>
<div class="embed-footer-box">
<p class="image is-20x20 customizable">
<a>
<img class="is-rounded preview-mode" src="">
</a>
</p>
<label class="is-sr-only" for="embedAuthor">Embed Author</label>
<textarea class="discord-embed-footer message-input autoresize preview-mode"
placeholder="Embed Footer..."
maxlength="2048" id="embedAuthor" name="embed_author" rows="1"></textarea>
</div>
</div>
</div>
<nav class="level is-mobile">
<div class="level-left">
<a class="level-item icon-toggle tts-toggle" title="Enable TTS">
<p>
TTS <span class="icon is-small"><i class="far fa-comment-lines"></i></span>
</p>
</a>
<a class="level-item icon-toggle autopin-toggle" title="Enable Autopin">
<p>
Pin <span class="icon is-small"><i class="far fa-thumbtack"></i></span>
</p>
</a>
<span style="width: 12px;"></span>
<a class="level-item set-color">
<p>
Set Embed Color <span class="icon is-small"><i
class="far fa-eye-dropper"></i></span>
</p>
</a>
<a class="level-item file-upload">
<div class="file">
<input class="file-input" type="file" name="attachment">
<p>
Attach File
<span class="icon is-small">
<i class="far fa-file-upload"></i>
</span>
</p>
</div>
</a>
<a class="level-item set-interval">
<p>
Set Interval <span class="icon is-small"><i class="far fa-repeat"></i></span>
</p>
</a>
<span style="width: 12px;"></span>
<a class="level-item preview-toggle" title="Preview Message">
<p>
Preview Mode <span class="icon is-small"><i class="far fa-eye"></i></span>
</p>
</a>
</div>
</nav>
<nav class="level is-mobile">
<div class="level-left">
<a class="level-item create-reminder" title="Create Reminder">
<p>
Create <span class="icon is-small"><i class="far fa-calendar-plus"></i></span>
</p>
</a>
<a class="level-item icon-toggle disable-reminder" title="Disable/enable Reminder">
<p>
Disable <span class="icon is-small"><i class="far fa-comment-slash"></i></span>
</p>
</a>
<a class="level-item delete-reminder" title="Delete Reminder">
<p>
Delete <span class="icon is-small"><i class="far fa-trash"></i></span>
</p>
</a>
</div>
</nav>
</div>
</article>
</div>

View File

@ -1,5 +1,4 @@
<div style="margin: 0 12px 12px 12px;">
<div class="create-reminder">
<div class="create-reminder">
<strong>Create Reminder</strong>
<div id="reminderCreator">
{% set creating = true %}
@ -30,7 +29,7 @@
<div class="column is-narrow">
<div class="control has-icons-left">
<div class="select is-small">
<select id="expandAll" style="width: 60px">
<select id="expandAll">
<option value="" selected></option>
<option value="expand">Expand All</option>
<option value="collapse">Collapse All</option>
@ -47,7 +46,6 @@
<div id="guildReminders">
</div>
</div>
</div>
<script src="/static/js/sort.js"></script>

View File

@ -1,23 +0,0 @@
<div style="margin: 0 12px 12px 12px;">
<div class="create-reminder">
<p>
<strong>Message Designer</strong>
</p>
{% include "reminder_dashboard/personal_reminder" %}
<p style="font-size: 0.8rem;">
Most fields are optional. Use 'Preview Mode' to see how the reminder will appear in Discord.
Scaling is not exact.
</p>
<div class="field">
<p class="control">
<a class="button is-success">
Create
</a>
</p>
</div>
</div>
<div id="personalReminders">
</div>
</div>