diff --git a/migration/04-reminder_templates.sql b/migration/04-reminder_templates.sql index b5ed77f..49c5bdc 100644 --- a/migration/04-reminder_templates.sql +++ b/migration/04-reminder_templates.sql @@ -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; diff --git a/web/src/lib.rs b/web/src/lib.rs index 8c54b49..5a93649 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -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, diff --git a/web/src/routes/dashboard/guild.rs b/web/src/routes/dashboard/guild.rs index 74de489..07220b2 100644 --- a/web/src/routes/dashboard/guild.rs +++ b/web/src/routes/dashboard/guild.rs @@ -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/templates")] +pub async fn get_reminder_templates( + id: u64, + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> 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//templates", data = "")] +pub async fn create_reminder_template( + id: u64, + reminder_template: Json, + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> 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//reminders", data = "")] pub async fn create_reminder( id: u64, @@ -550,9 +679,8 @@ pub async fn edit_reminder( } } -#[delete("/api/guild//reminders", data = "")] +#[delete("/api/guild/<_>/reminders", data = "")] pub async fn delete_reminder( - id: u64, reminder: Json, pool: &State>, ) -> JsonValue { diff --git a/web/src/routes/dashboard/mod.rs b/web/src/routes/dashboard/mod.rs index f533376..d6b1be0 100644 --- a/web/src/routes/dashboard/mod.rs +++ b/web/src/routes/dashboard/mod.rs @@ -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>, + attachment_name: Option, + avatar: Option, + content: String, + embed_author: String, + embed_author_url: Option, + embed_color: u32, + embed_description: String, + embed_footer: String, + embed_footer_url: Option, + embed_image_url: Option, + embed_thumbnail_url: Option, + embed_title: String, + embed_fields: Option>>, + tts: bool, + username: Option, +} + #[derive(Serialize, Deserialize)] pub struct EmbedField { title: String, @@ -241,3 +275,13 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result")] +pub async fn dashboard(cookies: &CookieJar<'_>) -> Result { + 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")) + } +} diff --git a/web/static/css/font.css b/web/static/css/font.css index 01b6e14..55de5c1 100644 --- a/web/static/css/font.css +++ b/web/static/css/font.css @@ -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; } diff --git a/web/static/css/style.css b/web/static/css/style.css index 9600be5..c7646b9 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -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; +} diff --git a/web/static/js/main.js b/web/static/js/main.js index c6a336e..ee8311f 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -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 $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 - ); - timeInput.value = localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss"); + let timeInput = frame.querySelector('input[name="time"]'); + 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], }) ); + + $createReminderBtn.querySelector("span.icon > i").classList = [ + "fas fa-check", + ]; + + window.setTimeout(() => { + $createReminderBtn.querySelector("span.icon > i").classList = [ + "fas fa-sparkles", + ]; + }, 1500); } - - $createBtn.querySelector("span.icon > i").classList = ["fas fa-check"]; - - window.setTimeout(() => { - $createBtn.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 = ""; diff --git a/web/templates/dashboard.html.tera b/web/templates/dashboard.html.tera index 3326749..b9891ea 100644 --- a/web/templates/dashboard.html.tera +++ b/web/templates/dashboard.html.tera @@ -6,6 +6,7 @@ + - - - -