jude/orphan-reminders #1
19
migrations/20230812111348_orphan_reminders.sql
Normal file
19
migrations/20230812111348_orphan_reminders.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- Drop existing constraint
|
||||||
|
ALTER TABLE `reminders` DROP CONSTRAINT `reminders_ibfk_1`;
|
||||||
|
|
||||||
|
ALTER TABLE `reminders` MODIFY COLUMN `channel_id` INT UNSIGNED;
|
||||||
|
ALTER TABLE `reminders` ADD COLUMN `guild_id` INT UNSIGNED;
|
||||||
|
|
||||||
|
ALTER TABLE `reminders`
|
||||||
|
ADD CONSTRAINT `guild_id_fk`
|
||||||
|
FOREIGN KEY (`guild_id`)
|
||||||
|
REFERENCES `guilds`(`id`)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE `reminders`
|
||||||
|
ADD CONSTRAINT `channel_id_fk`
|
||||||
|
FOREIGN KEY (`channel_id`)
|
||||||
|
REFERENCES `channels`(`id`)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
|
UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE reminders.channel_id = channels.id);
|
4
migrations/20230903131153_reminder_status_timing.sql
Normal file
4
migrations/20230903131153_reminder_status_timing.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE reminders ADD COLUMN `status_change_time` DATETIME;
|
||||||
|
|
||||||
|
-- This is a best-guess as to the status change time.
|
||||||
|
UPDATE reminders SET `status_change_time` = `utc_time`;
|
@ -310,6 +310,7 @@ WHERE
|
|||||||
reminders
|
reminders
|
||||||
WHERE
|
WHERE
|
||||||
reminders.`utc_time` <= NOW() AND
|
reminders.`utc_time` <= NOW() AND
|
||||||
|
reminders.`channel_id` IS NOT NULL AND
|
||||||
`status` = 'pending' AND
|
`status` = 'pending' AND
|
||||||
(
|
(
|
||||||
reminders.`interval_seconds` IS NOT NULL
|
reminders.`interval_seconds` IS NOT NULL
|
||||||
@ -471,7 +472,14 @@ WHERE
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) {
|
||||||
sqlx::query!("UPDATE reminders SET `status` = 'sent' WHERE `id` = ?", self.id)
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE reminders
|
||||||
|
SET `status` = 'sent', `status_change_time` = NOW()
|
||||||
|
WHERE `id` = ?
|
||||||
|
",
|
||||||
|
self.id
|
||||||
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.expect(&format!("Could not delete Reminder {}", self.id));
|
.expect(&format!("Could not delete Reminder {}", self.id));
|
||||||
|
@ -166,15 +166,21 @@ impl ComponentDataModel {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
ComponentDataModel::DelSelector(selector) => {
|
ComponentDataModel::DelSelector(selector) => {
|
||||||
let selected_id = component.data.values.join(",");
|
for id in &component.data.values {
|
||||||
|
match id.parse::<u32>() {
|
||||||
|
Ok(id) => {
|
||||||
|
if let Some(reminder) = Reminder::from_id(&data.database, id).await {
|
||||||
|
reminder.delete(&data.database).await.unwrap();
|
||||||
|
} else {
|
||||||
|
warn!("Attempt to delete non-existent reminder");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
Err(e) => {
|
||||||
"UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)",
|
warn!("Error casting ID to integer: {:?}.", e);
|
||||||
selected_id
|
}
|
||||||
)
|
}
|
||||||
.execute(&data.database)
|
}
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let reminders = Reminder::from_guild(
|
let reminders = Reminder::from_guild(
|
||||||
&ctx,
|
&ctx,
|
||||||
|
@ -10,6 +10,7 @@ pub struct ChannelData {
|
|||||||
pub webhook_id: Option<u64>,
|
pub webhook_id: Option<u64>,
|
||||||
pub webhook_token: Option<String>,
|
pub webhook_token: Option<String>,
|
||||||
pub paused: bool,
|
pub paused: bool,
|
||||||
|
pub db_guild_id: Option<u32>,
|
||||||
pub paused_until: Option<NaiveDateTime>,
|
pub paused_until: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +23,11 @@ impl ChannelData {
|
|||||||
|
|
||||||
if let Ok(c) = sqlx::query_as_unchecked!(
|
if let Ok(c) = sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?",
|
"
|
||||||
|
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until,
|
||||||
|
guild_id AS db_guild_id
|
||||||
|
FROM channels WHERE channel = ?
|
||||||
|
",
|
||||||
channel_id
|
channel_id
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
@ -30,12 +35,18 @@ impl ChannelData {
|
|||||||
{
|
{
|
||||||
Ok(c)
|
Ok(c)
|
||||||
} else {
|
} else {
|
||||||
let props = channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name));
|
let props =
|
||||||
|
channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name));
|
||||||
|
|
||||||
let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
|
let (guild_id, channel_name) =
|
||||||
|
if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))",
|
"
|
||||||
|
INSERT IGNORE INTO channels
|
||||||
|
(channel, name, guild_id)
|
||||||
|
VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
|
||||||
|
",
|
||||||
channel_id,
|
channel_id,
|
||||||
channel_name,
|
channel_name,
|
||||||
guild_id
|
guild_id
|
||||||
@ -46,7 +57,10 @@ impl ChannelData {
|
|||||||
Ok(sqlx::query_as_unchecked!(
|
Ok(sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
|
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused,
|
||||||
|
paused_until, guild_id AS db_guild_id
|
||||||
|
FROM channels
|
||||||
|
WHERE channel = ?
|
||||||
",
|
",
|
||||||
channel_id
|
channel_id
|
||||||
)
|
)
|
||||||
@ -58,9 +72,10 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u
|
|||||||
pub async fn commit_changes(&self, pool: &MySqlPool) {
|
pub async fn commit_changes(&self, pool: &MySqlPool) {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \
|
UPDATE channels
|
||||||
= ? WHERE id = ?
|
SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?,
|
||||||
",
|
paused = ?, paused_until = ?
|
||||||
|
WHERE id = ?",
|
||||||
self.name,
|
self.name,
|
||||||
self.nudge,
|
self.nudge,
|
||||||
self.blacklisted,
|
self.blacklisted,
|
||||||
|
@ -51,6 +51,7 @@ pub struct ReminderBuilder {
|
|||||||
pool: MySqlPool,
|
pool: MySqlPool,
|
||||||
uid: String,
|
uid: String,
|
||||||
channel: u32,
|
channel: u32,
|
||||||
|
guild: Option<u32>,
|
||||||
thread_id: Option<u64>,
|
thread_id: Option<u64>,
|
||||||
utc_time: NaiveDateTime,
|
utc_time: NaiveDateTime,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
@ -86,6 +87,7 @@ impl ReminderBuilder {
|
|||||||
INSERT INTO reminders (
|
INSERT INTO reminders (
|
||||||
`uid`,
|
`uid`,
|
||||||
`channel_id`,
|
`channel_id`,
|
||||||
|
`guild_id`,
|
||||||
`utc_time`,
|
`utc_time`,
|
||||||
`timezone`,
|
`timezone`,
|
||||||
`interval_seconds`,
|
`interval_seconds`,
|
||||||
@ -110,11 +112,13 @@ INSERT INTO reminders (
|
|||||||
?,
|
?,
|
||||||
?,
|
?,
|
||||||
?,
|
?,
|
||||||
|
?,
|
||||||
?
|
?
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.uid,
|
self.uid,
|
||||||
self.channel,
|
self.channel,
|
||||||
|
self.guild,
|
||||||
utc_time,
|
utc_time,
|
||||||
self.timezone,
|
self.timezone,
|
||||||
self.interval_seconds,
|
self.interval_seconds,
|
||||||
@ -247,10 +251,10 @@ impl<'a> MultiReminderBuilder<'a> {
|
|||||||
{
|
{
|
||||||
Err(ReminderError::UserBlockedDm)
|
Err(ReminderError::UserBlockedDm)
|
||||||
} else {
|
} else {
|
||||||
Ok(user_data.dm_channel)
|
Ok((user_data.dm_channel, None))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(user_data.dm_channel)
|
Ok((user_data.dm_channel, None))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ReminderError::InvalidTag)
|
Err(ReminderError::InvalidTag)
|
||||||
@ -297,13 +301,13 @@ impl<'a> MultiReminderBuilder<'a> {
|
|||||||
.commit_changes(&self.ctx.data().database)
|
.commit_changes(&self.ctx.data().database)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(channel_data.id)
|
Ok((channel_data.id, channel_data.db_guild_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(e) => Err(ReminderError::DiscordError(e.to_string())),
|
Err(e) => Err(ReminderError::DiscordError(e.to_string())),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(channel_data.id)
|
Ok((channel_data.id, channel_data.db_guild_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -317,7 +321,8 @@ impl<'a> MultiReminderBuilder<'a> {
|
|||||||
let builder = ReminderBuilder {
|
let builder = ReminderBuilder {
|
||||||
pool: self.ctx.data().database.clone(),
|
pool: self.ctx.data().database.clone(),
|
||||||
uid: generate_uid(),
|
uid: generate_uid(),
|
||||||
channel: c,
|
channel: c.0,
|
||||||
|
guild: c.1,
|
||||||
thread_id,
|
thread_id,
|
||||||
utc_time: self.utc_time,
|
utc_time: self.utc_time,
|
||||||
timezone: self.timezone.to_string(),
|
timezone: self.timezone.to_string(),
|
||||||
|
@ -304,7 +304,10 @@ WHERE
|
|||||||
&self,
|
&self,
|
||||||
db: impl Executor<'_, Database = Database>,
|
db: impl Executor<'_, Database = Database>,
|
||||||
) -> Result<(), sqlx::Error> {
|
) -> Result<(), sqlx::Error> {
|
||||||
sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", self.uid)
|
sqlx::query!(
|
||||||
|
"UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
|
||||||
|
self.uid
|
||||||
|
)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
@ -150,7 +150,8 @@ pub async fn initialize(
|
|||||||
.mount(
|
.mount(
|
||||||
"/dashboard",
|
"/dashboard",
|
||||||
routes![
|
routes![
|
||||||
routes::dashboard::dashboard,
|
routes::dashboard::dashboard_1,
|
||||||
|
routes::dashboard::dashboard_2,
|
||||||
routes::dashboard::dashboard_home,
|
routes::dashboard::dashboard_home,
|
||||||
routes::dashboard::user::get_user_info,
|
routes::dashboard::user::get_user_info,
|
||||||
routes::dashboard::user::update_user_info,
|
routes::dashboard::user::update_user_info,
|
||||||
|
@ -145,7 +145,7 @@ pub async fn import_reminders(
|
|||||||
attachment: record.attachment,
|
attachment: record.attachment,
|
||||||
attachment_name: record.attachment_name,
|
attachment_name: record.attachment_name,
|
||||||
avatar: record.avatar,
|
avatar: record.avatar,
|
||||||
channel: channel_id,
|
channel: Some(channel_id),
|
||||||
content: record.content,
|
content: record.content,
|
||||||
embed_author: record.embed_author,
|
embed_author: record.embed_author,
|
||||||
embed_author_url: record.embed_author_url,
|
embed_author_url: record.embed_author_url,
|
||||||
@ -171,6 +171,8 @@ pub async fn import_reminders(
|
|||||||
uid: generate_uid(),
|
uid: generate_uid(),
|
||||||
username: record.username,
|
username: record.username,
|
||||||
utc_time: record.utc_time,
|
utc_time: record.utc_time,
|
||||||
|
status: "pending".to_string(),
|
||||||
|
status_change_time: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
create_reminder(
|
create_reminder(
|
||||||
|
@ -318,30 +318,22 @@ pub async fn create_guild_reminder(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/guild/<id>/reminders")]
|
#[get("/api/guild/<id>/reminders?<status>")]
|
||||||
pub async fn get_reminders(
|
pub async fn get_reminders(
|
||||||
id: u64,
|
id: u64,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
ctx: &State<Context>,
|
|
||||||
serenity_context: &State<Context>,
|
serenity_context: &State<Context>,
|
||||||
pool: &State<Pool<MySql>>,
|
pool: &State<Pool<MySql>>,
|
||||||
|
status: Option<String>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
check_authorization!(cookies, serenity_context.inner(), id);
|
check_authorization!(cookies, serenity_context.inner(), id);
|
||||||
|
|
||||||
let channels_res = GuildId(id).channels(&ctx.inner()).await;
|
let status = status.unwrap_or("pending".to_string());
|
||||||
|
|
||||||
match channels_res {
|
|
||||||
Ok(channels) => {
|
|
||||||
let channels = channels
|
|
||||||
.keys()
|
|
||||||
.into_iter()
|
|
||||||
.map(|k| k.as_u64().to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
sqlx::query_as_unchecked!(
|
sqlx::query_as_unchecked!(
|
||||||
Reminder,
|
Reminder,
|
||||||
"SELECT
|
"
|
||||||
|
SELECT
|
||||||
reminders.attachment,
|
reminders.attachment,
|
||||||
reminders.attachment_name,
|
reminders.attachment_name,
|
||||||
reminders.avatar,
|
reminders.avatar,
|
||||||
@ -367,11 +359,14 @@ pub async fn get_reminders(
|
|||||||
reminders.tts,
|
reminders.tts,
|
||||||
reminders.uid,
|
reminders.uid,
|
||||||
reminders.username,
|
reminders.username,
|
||||||
reminders.utc_time
|
reminders.utc_time,
|
||||||
|
reminders.status,
|
||||||
|
reminders.status_change_time
|
||||||
FROM reminders
|
FROM reminders
|
||||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||||
WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
|
WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||||
channels
|
status,
|
||||||
|
id
|
||||||
)
|
)
|
||||||
.fetch_all(pool.inner())
|
.fetch_all(pool.inner())
|
||||||
.await
|
.await
|
||||||
@ -382,13 +377,6 @@ pub async fn get_reminders(
|
|||||||
json_err!("Could not load reminders")
|
json_err!("Could not load reminders")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
warn!("Could not fetch channels from {}: {:?}", id, e);
|
|
||||||
|
|
||||||
Ok(json!([]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
|
#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||||
pub async fn edit_reminder(
|
pub async fn edit_reminder(
|
||||||
@ -586,7 +574,9 @@ pub async fn edit_reminder(
|
|||||||
reminders.tts,
|
reminders.tts,
|
||||||
reminders.uid,
|
reminders.uid,
|
||||||
reminders.username,
|
reminders.username,
|
||||||
reminders.utc_time
|
reminders.utc_time,
|
||||||
|
reminders.status,
|
||||||
|
reminders.status_change_time
|
||||||
FROM reminders
|
FROM reminders
|
||||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||||
WHERE uid = ?",
|
WHERE uid = ?",
|
||||||
@ -610,7 +600,10 @@ pub async fn delete_reminder(
|
|||||||
reminder: Json<DeleteReminder>,
|
reminder: Json<DeleteReminder>,
|
||||||
pool: &State<Pool<MySql>>,
|
pool: &State<Pool<MySql>>,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid)
|
match sqlx::query!(
|
||||||
|
"UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?",
|
||||||
|
reminder.uid
|
||||||
|
)
|
||||||
.execute(pool.inner())
|
.execute(pool.inner())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -124,8 +124,8 @@ pub struct Reminder {
|
|||||||
attachment: Option<Vec<u8>>,
|
attachment: Option<Vec<u8>>,
|
||||||
attachment_name: Option<String>,
|
attachment_name: Option<String>,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
#[serde(with = "string")]
|
#[serde(with = "string_opt")]
|
||||||
channel: u64,
|
channel: Option<u64>,
|
||||||
content: String,
|
content: String,
|
||||||
embed_author: String,
|
embed_author: String,
|
||||||
embed_author_url: Option<String>,
|
embed_author_url: Option<String>,
|
||||||
@ -150,6 +150,8 @@ pub struct Reminder {
|
|||||||
uid: String,
|
uid: String,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
utc_time: NaiveDateTime,
|
utc_time: NaiveDateTime,
|
||||||
|
status: String,
|
||||||
|
status_change_time: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -308,6 +310,34 @@ mod string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod string_opt {
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
T: Display,
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
Some(value) => serializer.collect_str(value),
|
||||||
|
None => serializer.serialize_none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
T::Err: Display,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Option::deserialize(deserializer)?
|
||||||
|
.map(|d: String| d.parse().map_err(de::Error::custom))
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod base64s {
|
mod base64s {
|
||||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
@ -372,7 +402,7 @@ pub async fn create_reminder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate channel
|
// validate channel
|
||||||
let channel = ChannelId(reminder.channel).to_channel_cached(&ctx);
|
let channel = reminder.channel.map(|c| ChannelId(c).to_channel_cached(&ctx)).flatten();
|
||||||
let channel_exists = channel.is_some();
|
let channel_exists = channel.is_some();
|
||||||
|
|
||||||
let channel_matches_guild =
|
let channel_matches_guild =
|
||||||
@ -380,14 +410,14 @@ pub async fn create_reminder(
|
|||||||
|
|
||||||
if !channel_matches_guild || !channel_exists {
|
if !channel_matches_guild || !channel_exists {
|
||||||
warn!(
|
warn!(
|
||||||
"Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})",
|
"Error in `create_reminder`: channel {:?} not found for guild {} (channel exists: {})",
|
||||||
reminder.channel, guild_id, channel_exists
|
reminder.channel, guild_id, channel_exists
|
||||||
);
|
);
|
||||||
|
|
||||||
return Err(json!({"error": "Channel not found"}));
|
return Err(json!({"error": "Channel not found"}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel = create_database_channel(&ctx, ChannelId(reminder.channel), pool).await;
|
let channel = create_database_channel(&ctx, ChannelId(reminder.channel.unwrap()), pool).await;
|
||||||
|
|
||||||
if let Err(e) = channel {
|
if let Err(e) = channel {
|
||||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||||
@ -479,6 +509,7 @@ pub async fn create_reminder(
|
|||||||
attachment,
|
attachment,
|
||||||
attachment_name,
|
attachment_name,
|
||||||
channel_id,
|
channel_id,
|
||||||
|
guild_id,
|
||||||
avatar,
|
avatar,
|
||||||
content,
|
content,
|
||||||
embed_author,
|
embed_author,
|
||||||
@ -501,11 +532,12 @@ pub async fn create_reminder(
|
|||||||
tts,
|
tts,
|
||||||
username,
|
username,
|
||||||
`utc_time`
|
`utc_time`
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
new_uid,
|
new_uid,
|
||||||
attachment_data,
|
attachment_data,
|
||||||
reminder.attachment_name,
|
reminder.attachment_name,
|
||||||
channel,
|
channel,
|
||||||
|
guild_id.0,
|
||||||
reminder.avatar,
|
reminder.avatar,
|
||||||
reminder.content,
|
reminder.content,
|
||||||
reminder.embed_author,
|
reminder.embed_author,
|
||||||
@ -560,7 +592,9 @@ pub async fn create_reminder(
|
|||||||
reminders.tts,
|
reminders.tts,
|
||||||
reminders.uid,
|
reminders.uid,
|
||||||
reminders.username,
|
reminders.username,
|
||||||
reminders.utc_time
|
reminders.utc_time,
|
||||||
|
reminders.status,
|
||||||
|
reminders.status_change_time
|
||||||
FROM reminders
|
FROM reminders
|
||||||
LEFT JOIN channels ON channels.id = reminders.channel_id
|
LEFT JOIN channels ON channels.id = reminders.channel_id
|
||||||
WHERE uid = ?",
|
WHERE uid = ?",
|
||||||
@ -662,7 +696,17 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirec
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<_>")]
|
#[get("/<_>")]
|
||||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
pub async fn dashboard_1(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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/<_>/<_>")]
|
||||||
|
pub async fn dashboard_2(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
|
||||||
if cookies.get_private("userid").is_some() {
|
if cookies.get_private("userid").is_some() {
|
||||||
let map: HashMap<&str, String> = HashMap::new();
|
let map: HashMap<&str, String> = HashMap::new();
|
||||||
Ok(Template::render("dashboard", &map))
|
Ok(Template::render("dashboard", &map))
|
||||||
|
@ -291,10 +291,19 @@ div.dashboard-sidebar:not(.mobile-sidebar) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.guildList {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
|
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
|
||||||
position: fixed;
|
flex-shrink: 0;
|
||||||
bottom: 0;
|
flex-grow: 0;
|
||||||
width: 226px;
|
}
|
||||||
|
|
||||||
|
div.dashboard-sidebar svg {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.mobile-sidebar {
|
div.mobile-sidebar {
|
||||||
@ -679,6 +688,76 @@ li.highlight {
|
|||||||
|
|
||||||
/* END */
|
/* END */
|
||||||
|
|
||||||
|
div.reminderError {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorIcon {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorIcon .fas {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="deleted"] .errorIcon {
|
||||||
|
background-color: #e7e5e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="failed"] .errorIcon {
|
||||||
|
background-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="sent"] .errorIcon {
|
||||||
|
background-color: #d9f99d;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead .reminderName {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(54, 54, 54);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderError .errorHead .reminderTime {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 1;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(54, 54, 54);
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-color: #e5e5e5;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
/* other stuff */
|
/* other stuff */
|
||||||
|
|
||||||
.half-rem {
|
.half-rem {
|
||||||
@ -716,6 +795,18 @@ a.switch-pane {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guild-submenu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guild-submenu li {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.switch-pane.is-active ~ .guild-submenu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
background-color: #5865F2;
|
background-color: #5865F2;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ const $downloader = document.querySelector("a#downloader");
|
|||||||
const $uploader = document.querySelector("input#uploader");
|
const $uploader = document.querySelector("input#uploader");
|
||||||
|
|
||||||
let channels = [];
|
let channels = [];
|
||||||
|
let reminderErrors = [];
|
||||||
let guildNames = {};
|
let guildNames = {};
|
||||||
let roles = [];
|
let roles = [];
|
||||||
let templates = {};
|
let templates = {};
|
||||||
@ -33,7 +34,11 @@ let globalPatreon = false;
|
|||||||
let guildPatreon = false;
|
let guildPatreon = false;
|
||||||
|
|
||||||
function guildId() {
|
function guildId() {
|
||||||
return document.querySelector(".guildList a.is-active").dataset["guild"];
|
return document.querySelector("li > a.is-active").parentElement.dataset["guild"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function guildName() {
|
||||||
|
return guildNames[guildId()];
|
||||||
}
|
}
|
||||||
|
|
||||||
function colorToInt(r, g, b) {
|
function colorToInt(r, g, b) {
|
||||||
@ -52,7 +57,7 @@ function switch_pane(selector) {
|
|||||||
el.classList.add("is-hidden");
|
el.classList.add("is-hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById(selector).classList.remove("is-hidden");
|
document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_select(sel) {
|
function update_select(sel) {
|
||||||
@ -449,21 +454,19 @@ document.addEventListener("guildSwitched", async (e) => {
|
|||||||
.querySelectorAll(".patreon-only")
|
.querySelectorAll(".patreon-only")
|
||||||
.forEach((el) => el.classList.add("is-locked"));
|
.forEach((el) => el.classList.add("is-locked"));
|
||||||
|
|
||||||
let $anchor = document.querySelector(
|
let $li = document.querySelector(`li[data-guild="${e.detail.guild_id}"]`);
|
||||||
`.switch-pane[data-guild="${e.detail.guild_id}"]`
|
|
||||||
);
|
|
||||||
|
|
||||||
let hasError = false;
|
if ($li === null) {
|
||||||
|
|
||||||
if ($anchor === null) {
|
|
||||||
switch_pane("user-error");
|
switch_pane("user-error");
|
||||||
hasError = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch_pane($anchor.dataset["pane"]);
|
switch_pane(e.detail.pane);
|
||||||
reset_guild_pane();
|
reset_guild_pane();
|
||||||
$anchor.classList.add("is-active");
|
$li.querySelector("li > a").classList.add("is-active");
|
||||||
|
$li.querySelectorAll(`*[data-pane="${e.detail.pane}"]`).forEach((el) => {
|
||||||
|
el.classList.add("is-active");
|
||||||
|
});
|
||||||
|
|
||||||
if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
|
if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
|
||||||
document
|
document
|
||||||
@ -471,15 +474,26 @@ document.addEventListener("guildSwitched", async (e) => {
|
|||||||
.forEach((el) => el.classList.remove("is-locked"));
|
.forEach((el) => el.classList.remove("is-locked"));
|
||||||
}
|
}
|
||||||
|
|
||||||
hasError = await fetch_channels(e.detail.guild_id);
|
const event = new CustomEvent("paneLoad", {
|
||||||
|
detail: {
|
||||||
|
guild_id: e.detail.guild_id,
|
||||||
|
pane: e.detail.pane,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("paneLoad", async (ev) => {
|
||||||
|
const hasError = await fetch_channels(ev.detail.guild_id);
|
||||||
if (!hasError) {
|
if (!hasError) {
|
||||||
fetch_roles(e.detail.guild_id);
|
fetch_roles(ev.detail.guild_id);
|
||||||
fetch_templates(e.detail.guild_id);
|
fetch_templates(ev.detail.guild_id);
|
||||||
fetch_reminders(e.detail.guild_id);
|
fetch_reminders(ev.detail.guild_id);
|
||||||
|
|
||||||
document.querySelectorAll("p.pageTitle").forEach((el) => {
|
document.querySelectorAll("p.pageTitle").forEach((el) => {
|
||||||
el.textContent = `${e.detail.guild_name} Reminders`;
|
el.textContent = `${guildName()} Reminders`;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll("select.channel-selector").forEach((el) => {
|
document.querySelectorAll("select.channel-selector").forEach((el) => {
|
||||||
el.addEventListener("change", (e) => {
|
el.addEventListener("change", (e) => {
|
||||||
update_select(e.target);
|
update_select(e.target);
|
||||||
@ -684,36 +698,56 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
"%guildname%",
|
"%guildname%",
|
||||||
guild.name
|
guild.name
|
||||||
);
|
);
|
||||||
$anchor.dataset["guild"] = guild.id;
|
|
||||||
$anchor.dataset["name"] = guild.name;
|
$anchor.dataset["name"] = guild.name;
|
||||||
$anchor.href = `/dashboard/${guild.id}?name=${guild.name}`;
|
$anchor.href = `/dashboard/${guild.id}/reminders`;
|
||||||
|
|
||||||
$anchor.addEventListener("click", async (e) => {
|
const $li = $anchor.parentElement;
|
||||||
|
$li.dataset["guild"] = guild.id;
|
||||||
|
|
||||||
|
$li.querySelectorAll("a").forEach((el) => {
|
||||||
|
el.addEventListener("click", (e) => {
|
||||||
|
const pane = el.dataset["pane"];
|
||||||
|
const slug = el.dataset["slug"];
|
||||||
|
|
||||||
|
if (pane !== undefined && slug !== undefined) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.history.pushState({}, "", `/dashboard/${guild.id}`);
|
|
||||||
|
switch_pane(pane);
|
||||||
|
|
||||||
|
window.history.pushState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`/dashboard/${guild.id}/${slug}`
|
||||||
|
);
|
||||||
const event = new CustomEvent("guildSwitched", {
|
const event = new CustomEvent("guildSwitched", {
|
||||||
detail: {
|
detail: {
|
||||||
guild_name: guild.name,
|
|
||||||
guild_id: guild.id,
|
guild_id: guild.id,
|
||||||
|
pane,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
element.append($clone);
|
element.append($clone);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches = window.location.href.match(/dashboard\/(\d+)/);
|
const matches = window.location.href.match(
|
||||||
|
/dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/
|
||||||
|
);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
let id = matches[1];
|
let id = matches[1];
|
||||||
|
let kind = matches[3];
|
||||||
let name = guildNames[id];
|
let name = guildNames[id];
|
||||||
|
|
||||||
const event = new CustomEvent("guildSwitched", {
|
const event = new CustomEvent("guildSwitched", {
|
||||||
detail: {
|
detail: {
|
||||||
guild_name: name,
|
guild_name: name,
|
||||||
guild_id: id,
|
guild_id: id,
|
||||||
|
pane: kind,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
43
web/static/js/reminder_errors.js
Normal file
43
web/static/js/reminder_errors.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
function loadErrors() {
|
||||||
|
return fetch(
|
||||||
|
`/dashboard/api/guild/${guildId()}/reminders?status=deleted,sent,failed`
|
||||||
|
).then((response) => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("paneLoad", (ev) => {
|
||||||
|
if (ev.detail.pane !== "errors") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(".reminderError").forEach((el) => el.remove());
|
||||||
|
|
||||||
|
const template = document.getElementById("reminderError");
|
||||||
|
const container = document.getElementById("reminderLog");
|
||||||
|
|
||||||
|
loadErrors()
|
||||||
|
.then((res) => {
|
||||||
|
res = res
|
||||||
|
.filter((r) => r.status_change_time !== null)
|
||||||
|
.sort((a, b) => a.status_change_time < b.status_change_time);
|
||||||
|
|
||||||
|
for (const reminder of res) {
|
||||||
|
const newRow = template.content.cloneNode(true);
|
||||||
|
|
||||||
|
newRow.querySelector(".reminderError").dataset["case"] = reminder.status;
|
||||||
|
|
||||||
|
const statusTime = new luxon.DateTime.fromISO(
|
||||||
|
reminder.status_change_time,
|
||||||
|
{ zone: "UTC" }
|
||||||
|
);
|
||||||
|
newRow.querySelector(".reminderName").textContent = reminder.name;
|
||||||
|
newRow.querySelector(".reminderTime").textContent = statusTime
|
||||||
|
.toLocal()
|
||||||
|
.toLocaleString(luxon.DateTime.DATETIME_MED);
|
||||||
|
|
||||||
|
container.appendChild(newRow);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
$loader.classList.add("is-hidden");
|
||||||
|
});
|
||||||
|
});
|
@ -1,19 +0,0 @@
|
|||||||
let _reminderErrors = [];
|
|
||||||
|
|
||||||
const reminderErrors = () => {
|
|
||||||
return _reminderErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const guildId = () => {
|
|
||||||
let selected: HTMLElement = document.querySelector(".guildList a.is-active");
|
|
||||||
return selected.dataset["guild"];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function loadErrors() {
|
|
||||||
fetch(`/dashboard/api/guild/${guildId()}/errors`).then(response => response.json())
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
|
|
||||||
})
|
|
@ -332,16 +332,16 @@
|
|||||||
<p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> 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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section id="guild" class="is-hidden">
|
<section data-name="reminders" class="is-hidden">
|
||||||
{% include "reminder_dashboard/reminder_dashboard" %}
|
{% include "reminder_dashboard/reminder_dashboard" %}
|
||||||
</section>
|
</section>
|
||||||
<section id="reminder-errors" class="is-hidden">
|
<section data-name="errors" class="is-hidden">
|
||||||
{% include "reminder_dashboard/reminder_errors" %}
|
{% include "reminder_dashboard/reminder_errors" %}
|
||||||
</section>
|
</section>
|
||||||
<section id="guild-error" class="is-hidden">
|
<section data-name="guild-error" class="is-hidden">
|
||||||
{% include "reminder_dashboard/guild_error" %}
|
{% include "reminder_dashboard/guild_error" %}
|
||||||
</section>
|
</section>
|
||||||
<section id="user-error" class="is-hidden">
|
<section data-name="user-error" class="is-hidden">
|
||||||
{% include "reminder_dashboard/user_error" %}
|
{% include "reminder_dashboard/user_error" %}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -375,14 +375,28 @@
|
|||||||
|
|
||||||
<template id="guildListEntry">
|
<template id="guildListEntry">
|
||||||
<li>
|
<li>
|
||||||
<a class="switch-pane" data-pane="guild">
|
<a class="switch-pane" data-pane="reminders" data-slug="reminders">
|
||||||
<span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span>
|
<span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span>
|
||||||
</a>
|
</a>
|
||||||
|
<ul class="guild-submenu">
|
||||||
|
<li>
|
||||||
|
<a class="switch-pane" data-pane="reminders" data-slug="reminders">
|
||||||
|
<span class="icon"><i class="fas fa-calendar-alt"></i></span> Reminders
|
||||||
|
</a>
|
||||||
|
<a class="switch-pane" data-pane="errors" data-slug="errors">
|
||||||
|
<span class="icon"><i class="fas fa-file-alt"></i></span> Logs
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="guildReminder">
|
<template id="guildReminder">
|
||||||
{% include "reminder_dashboard/guild_reminder" %}
|
{% include "reminder_dashboard/templates/guild_reminder" %}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="reminderError">
|
||||||
|
{% include "reminder_dashboard/templates/reminder_error" %}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="/static/js/iro.js"></script>
|
<script src="/static/js/iro.js"></script>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<strong>Create Reminder</strong>
|
<strong>Create Reminder</strong>
|
||||||
<div id="reminderCreator">
|
<div id="reminderCreator">
|
||||||
{% set creating = true %}
|
{% set creating = true %}
|
||||||
{% include "reminder_dashboard/guild_reminder" %}
|
{% include "reminder_dashboard/templates/guild_reminder" %}
|
||||||
{% set creating = false %}
|
{% set creating = false %}
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
@ -46,6 +46,10 @@
|
|||||||
<div id="guildReminders">
|
<div id="guildReminders">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="guildErrors">
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/sort.js"></script>
|
<script src="/static/js/sort.js"></script>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div>
|
<div id="reminderLog">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
<div class="reminderError" data-case="success">
|
||||||
|
<div class="errorHead">
|
||||||
|
<div class="errorIcon">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="reminderName">
|
||||||
|
Reminder
|
||||||
|
</div>
|
||||||
|
<div class="reminderTime">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
Loading…
Reference in New Issue
Block a user