moved dashboard crate into here
1341
Cargo.lock
generated
@ -31,6 +31,9 @@ path = "command_attributes"
|
|||||||
[dependencies.postman]
|
[dependencies.postman]
|
||||||
path = "postman"
|
path = "postman"
|
||||||
|
|
||||||
|
[dependencies.reminder_web]
|
||||||
|
path = "web"
|
||||||
|
|
||||||
[dependencies.serenity]
|
[dependencies.serenity]
|
||||||
git = "https://github.com/serenity-rs/serenity"
|
git = "https://github.com/serenity-rs/serenity"
|
||||||
branch = "next"
|
branch = "next"
|
||||||
|
27
Rocket.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[default]
|
||||||
|
address = "0.0.0.0"
|
||||||
|
port = 5000
|
||||||
|
template_dir = "web/templates"
|
||||||
|
|
||||||
|
[debug]
|
||||||
|
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
|
||||||
|
|
||||||
|
[default.tls]
|
||||||
|
certs = "web/private/rsa_sha256_cert.pem"
|
||||||
|
key = "web/private/rsa_sha256_key.pem"
|
||||||
|
|
||||||
|
[rsa_sha256.tls]
|
||||||
|
certs = "web/private/rsa_sha256_cert.pem"
|
||||||
|
key = "web/private/rsa_sha256_key.pem"
|
||||||
|
|
||||||
|
[ecdsa_nistp256_sha256.tls]
|
||||||
|
certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
|
||||||
|
key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
|
||||||
|
|
||||||
|
[ecdsa_nistp384_sha384.tls]
|
||||||
|
certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
|
||||||
|
key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
|
||||||
|
|
||||||
|
[ed25519.tls]
|
||||||
|
certs = "web/private/ed25519_cert.pem"
|
||||||
|
key = "eb/private/ed25519_key.pem"
|
14
src/main.rs
@ -22,7 +22,6 @@ use std::{
|
|||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use log::info;
|
use log::info;
|
||||||
use postman::initialize;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
async_trait,
|
async_trait,
|
||||||
client::Client,
|
client::Client,
|
||||||
@ -83,11 +82,18 @@ impl EventHandler for Handler {
|
|||||||
info!("Preparing to send reminders");
|
info!("Preparing to send reminders");
|
||||||
|
|
||||||
if !self.is_loop_running.load(Ordering::Relaxed) {
|
if !self.is_loop_running.load(Ordering::Relaxed) {
|
||||||
let ctx = ctx_base.clone();
|
let ctx1 = ctx_base.clone();
|
||||||
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
let ctx2 = ctx_base.clone();
|
||||||
|
|
||||||
|
let pool1 = ctx1.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
let pool2 = ctx2.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
initialize(ctx, &pool).await;
|
postman::initialize(ctx1, &pool1).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
reminder_web::initialize(ctx2, pool2).await.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.is_loop_running.swap(true, Ordering::Relaxed);
|
self.is_loop_running.swap(true, Ordering::Relaxed);
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "web"
|
name = "reminder_web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||||
|
edition = "2018"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] }
|
||||||
|
rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] }
|
||||||
|
serenity = { git = "https://github.com/serenity-rs/serenity", branch = "next", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
|
||||||
|
oauth2 = "4"
|
||||||
|
log = "0.4"
|
||||||
|
reqwest = "0.11"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono"] }
|
||||||
|
chrono = "0.4"
|
||||||
|
chrono-tz = "0.5"
|
||||||
|
32
web/private/ca_cert.pem
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
I4/u
|
||||||
|
-----END CERTIFICATE-----
|
51
web/private/ca_key.pem
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
-----END RSA PRIVATE KEY-----
|
21
web/private/ecdsa_nistp256_sha256_cert.pem
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDYTCCAUmgAwIBAgIUA/EaCcXKgOxBiuaMNTackeF9DgcwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
|
||||||
|
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
|
||||||
|
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
|
||||||
|
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49
|
||||||
|
AwEHA0IABOEVdmVd218y3Yy+ocjtt5siaFsNR7tknvS21pzKzxsFc05UrF74YqRx
|
||||||
|
Ic6/AQq56C48x6gDhUCdf+nMzD0NWTijGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9z
|
||||||
|
dDANBgkqhkiG9w0BAQsFAAOCAgEAW32kadHWEIsN5WXacjtgE9jbFii/rWJjrAO/
|
||||||
|
GWuARwLvjWeEFM5xSTny1cTDEz/7Bmfd9GRRpfgxKCFBGaU7zTmOV/AokrjUay+s
|
||||||
|
KPj0EV9ZE/84KVfr+MOuhwXhSSv+NvxduFw1sdhTUHs+Wopwjte+bnOUBXmnQH97
|
||||||
|
ITSQuUvEamPUS4tJD/kQ0qSGJKNv3CShhbe3XkjUd0UKK7lZzn6hY2fy3CrkkJDT
|
||||||
|
GyzFCRf3ZDfjW5/fGXN/TNoEK65pPQVr5taK/bovDesEO7rR4Y4YgtnGlJ7YToWh
|
||||||
|
E6I77CbsJCJdmgpjPNwRlwQ8upoWAfxOHqwg32s6uWq20v4TXZTOnEKOPEJG74bh
|
||||||
|
JHtwqsy2fIdGz4kZksKGerzJffyQPhX4f3qxxrijT9Hj2EoFIQ4u/jTRpK31IW3R
|
||||||
|
gEeNB8O4iyg3crx0oHRwc6zX63O6wBJCZ+0uxLoyjwtjKCxVYbJpccjoQh6zO3UO
|
||||||
|
pqAO+NKxQ469QDBV3uRyyQ7DRPu6p67/8gn1E/o8kyU1P7eLFIhBp4T4wCaMQBG6
|
||||||
|
IpL5W7eCyuOcqfdm471N07Z4KAqiV8eBJcKaAE+WzNIvrFvCZ/zq7Gj8p07nkjK8
|
||||||
|
+ZGDUieiY0mQEpiXLQZt1xNtT/MmlAzFYrK7t6gSygykQKjO1o4GG4g+eXu3h1YK
|
||||||
|
avsOwtc=
|
||||||
|
-----END CERTIFICATE-----
|
5
web/private/ecdsa_nistp256_sha256_key_pkcs8.pem
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeo/7wBIYVax9bj4m
|
||||||
|
1UEe2H+H4YLsbApDCZMslZbubRuhRANCAAThFXZlXdtfMt2MvqHI7bebImhbDUe7
|
||||||
|
ZJ70ttacys8bBXNOVKxe+GKkcSHOvwEKueguPMeoA4VAnX/pzMw9DVk4
|
||||||
|
-----END PRIVATE KEY-----
|
21
web/private/ecdsa_nistp384_sha384_cert.pem
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDfjCCAWagAwIBAgIUfmkM99JpQUsQsh8TVILPQDBGOhowDQYJKoZIhvcNAQEM
|
||||||
|
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
|
||||||
|
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
|
||||||
|
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
|
||||||
|
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDB2MBAGByqGSM49AgEGBSuBBAAi
|
||||||
|
A2IABM5rQQNZEGWeDyrg2dVQS15c+JsX/ginN9/ArHpTgGO6xaLfkbekIj7gewUR
|
||||||
|
VQcQrYulwu02HTFDzGVvf1DdCZzIJLJUpl+jagdI05yMPFg2s5lThzGB6HENyw1I
|
||||||
|
hU1dMaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IC
|
||||||
|
AQAzpXFEeCMvTBG+kzmHN/+esjCK8MO/Grrw3Qoa1stX0yjNWzfhlGHfWk9Mgwnp
|
||||||
|
DnIEFT9lB+nT9uTkKL81Opu2bkWXuL0MyjyYmcCfEgJeDblUbD4HYzPO/s2P7QZu
|
||||||
|
Oa1pNKBiSWe1xq+F/gkn0+nDfICq3QQOOQMzaAmlFmszN3v7pryz+x21MqTFsfuW
|
||||||
|
ymycRcwZ3VyYeceZuBxjOhR2l8I5yZbR+KPj8VS7A3vgqvkS8ixTK0LrxcLwrTIz
|
||||||
|
W9Z1skzVpux4K7iwn3qlWIJ7EbERi9Ydz0MzpjD+CHxGlgHTzT552DGZhPO8D7BE
|
||||||
|
+4IoS0YeXEGLeC2a9Hmiy3FXbM4nt3aIWMiWyhjslXIbvWOJL1Vi5GNpdQCG+rB7
|
||||||
|
lvA0IISp/tAi9duufdONjgRceRDsqf7o6TOBQdB3QxHRt9gCB2QquRh5llMUY8YH
|
||||||
|
PxFJAEzF4X5b0q0k4b50HRVNfDY0SC/XIR7S2xnLDGuoUqrOpUligBzfXV4JHrPv
|
||||||
|
YKHsWSOiVxU9eY1SYKuKqnOsGvXSSQlYqSK1ol94eOKDjNy8wekaVYAQNsdNL7o5
|
||||||
|
QSWZUcbZLkaNMFdD9twWGduAs0eTichVgyvRgPdevjTGDPBHKNSUURZRTYeW4aIJ
|
||||||
|
QzSQUQXwOXsQOmfCkaaTISsDwZAJ0wFqqST1AOhlCWD1OQ==
|
||||||
|
-----END CERTIFICATE-----
|
6
web/private/ecdsa_nistp384_sha384_key_pkcs8.pem
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCRBt7MeJXyJRq7grGQ
|
||||||
|
jEdBHE31hG5LuKQ5iL8yTVGk75Kc8ISll2LewbvQ3NhR6E+hZANiAATOa0EDWRBl
|
||||||
|
ng8q4NnVUEteXPibF/4IpzffwKx6U4BjusWi35G3pCI+4HsFEVUHEK2LpcLtNh0x
|
||||||
|
Q8xlb39Q3QmcyCSyVKZfo2oHSNOcjDxYNrOZU4cxgehxDcsNSIVNXTE=
|
||||||
|
-----END PRIVATE KEY-----
|
20
web/private/ed25519_cert.pem
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDMjCCARqgAwIBAgIUSu1CGfaWlNPjjLG7SaEEgxI+AsAwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
|
||||||
|
Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
|
||||||
|
MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
|
||||||
|
DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUGAytlcAMhAKNv1fkv5rxY
|
||||||
|
xGT84C98g2xPpain+jsliHvYrqNkS1zhoxgwFjAUBgNVHREEDTALgglsb2NhbGhv
|
||||||
|
c3QwDQYJKoZIhvcNAQELBQADggIBAMgWZhr3whYbFPNYm5RZxjgEonMY7cH/Rza1
|
||||||
|
UrBkyoMRL9v00YKJTEvjl5Xl4ku7jqxY2qjValacDroD8hYTLhhkkbHxH6u+kJSC
|
||||||
|
cKRQ8QVBZMmoor8qD2Y2BuXZYGWEtgeoXh+DLHWOim7gXDD6GyFPnYFGHnRhNmkE
|
||||||
|
6fPcfw1FZmBrMM9rk31EeK9nLVRabL+vuLDEddX1LH4LwK4OuV51wQMZGYiC3M2b
|
||||||
|
JpmuPAUAUyiyN53Utuw/ubeih3cEglOwCyDPFt6XISYHO0UaQdw25uhpjxs8KTZB
|
||||||
|
qOPJAI4cvGaN3GARXvz2P/QHUiTzsf2XOXXtrO0g+F9P7t6x2xL35c0A8yxKkfsa
|
||||||
|
RKdCveRuVlsLXVLVBikqLE/OgPC6SIhIFvTYzXTaZyNfge6dRy2Wg6qWPcGwseeA
|
||||||
|
QpFKgWiY3cuy7nJm6uybR6zOEMj5GgNWIbICGguQ5161MLnv0bEmKh2d9/jQ/XN5
|
||||||
|
M+nixtGla/G4hL3FQIy9rhHVZzPWwSxl+/eE4qgnInEhmlQ2KrcpgneCt38t0vjJ
|
||||||
|
dwHZSzVv8yX7fE0OSq/DWQXJraWUcT7Ds0g7T7jWkWfS0qStVd9VJ+rYQ8dBbE9Y
|
||||||
|
gcmkm1DMUd+y9xDRDjxby7gMDWFiN2Au9FQgaIpIctTNCj0+agFBPnfXPVSIH6gX
|
||||||
|
10kA2ZVX
|
||||||
|
-----END CERTIFICATE-----
|
3
web/private/ed25519_key.pem
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIGtLkaYE4D+P5HLkoc3a/tNhPP+gnhPLF3jEtdYcAtpd
|
||||||
|
-----END PRIVATE KEY-----
|
114
web/private/gen_certs.sh
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# ./gen_certs.sh [cert-kind]
|
||||||
|
#
|
||||||
|
# [cert-kind]:
|
||||||
|
# ed25519
|
||||||
|
# rsa_sha256
|
||||||
|
# ecdsa_nistp256_sha256
|
||||||
|
# ecdsa_nistp384_sha384
|
||||||
|
#
|
||||||
|
# Generate a certificate of the [cert-kind] key type, or if no cert-kind is
|
||||||
|
# specified, all of the certificates.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./gen_certs.sh ed25519
|
||||||
|
# ./gen_certs.sh rsa_sha256
|
||||||
|
|
||||||
|
# TODO: `rustls` (really, `webpki`) doesn't currently use the CN in the subject
|
||||||
|
# to check if a certificate is valid for a server name sent via SNI. It's not
|
||||||
|
# clear if this is intended, since certificates _should_ have a `subjectAltName`
|
||||||
|
# with a DNS name, or if it simply hasn't been implemented yet. See
|
||||||
|
# https://bugzilla.mozilla.org/show_bug.cgi?id=552346 for a bit more info.
|
||||||
|
|
||||||
|
CA_SUBJECT="/C=US/ST=CA/O=Rocket CA/CN=Rocket Root CA"
|
||||||
|
SUBJECT="/C=US/ST=CA/O=Rocket/CN=localhost"
|
||||||
|
ALT="DNS:localhost"
|
||||||
|
|
||||||
|
function gen_ca() {
|
||||||
|
openssl genrsa -out ca_key.pem 4096
|
||||||
|
openssl req -new -x509 -days 3650 -key ca_key.pem \
|
||||||
|
-subj "${CA_SUBJECT}" -out ca_cert.pem
|
||||||
|
}
|
||||||
|
|
||||||
|
function gen_ca_if_non_existent() {
|
||||||
|
if ! [ -f ./ca_cert.pem ]; then gen_ca; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function gen_rsa_sha256() {
|
||||||
|
gen_ca_if_non_existent
|
||||||
|
|
||||||
|
openssl req -newkey rsa:4096 -nodes -sha256 -keyout rsa_sha256_key.pem \
|
||||||
|
-subj "${SUBJECT}" -out server.csr
|
||||||
|
|
||||||
|
openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
|
||||||
|
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
|
||||||
|
-in server.csr -out rsa_sha256_cert.pem
|
||||||
|
|
||||||
|
rm ca_cert.srl server.csr
|
||||||
|
}
|
||||||
|
|
||||||
|
function gen_ed25519() {
|
||||||
|
gen_ca_if_non_existent
|
||||||
|
|
||||||
|
openssl genpkey -algorithm ED25519 > ed25519_key.pem
|
||||||
|
|
||||||
|
openssl req -new -key ed25519_key.pem -subj "${SUBJECT}" -out server.csr
|
||||||
|
openssl x509 -req -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
|
||||||
|
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
|
||||||
|
-in server.csr -out ed25519_cert.pem
|
||||||
|
|
||||||
|
rm ca_cert.srl server.csr
|
||||||
|
}
|
||||||
|
|
||||||
|
function gen_ecdsa_nistp256_sha256() {
|
||||||
|
gen_ca_if_non_existent
|
||||||
|
|
||||||
|
openssl ecparam -out ecdsa_nistp256_sha256_key.pem -name prime256v1 -genkey
|
||||||
|
|
||||||
|
# Convert to pkcs8 format supported by rustls
|
||||||
|
openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp256_sha256_key.pem \
|
||||||
|
-out ecdsa_nistp256_sha256_key_pkcs8.pem
|
||||||
|
|
||||||
|
openssl req -new -nodes -sha256 -key ecdsa_nistp256_sha256_key_pkcs8.pem \
|
||||||
|
-subj "${SUBJECT}" -out server.csr
|
||||||
|
|
||||||
|
openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
|
||||||
|
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
|
||||||
|
-in server.csr -out ecdsa_nistp256_sha256_cert.pem
|
||||||
|
|
||||||
|
rm ca_cert.srl server.csr ecdsa_nistp256_sha256_key.pem
|
||||||
|
}
|
||||||
|
|
||||||
|
function gen_ecdsa_nistp384_sha384() {
|
||||||
|
gen_ca_if_non_existent
|
||||||
|
|
||||||
|
openssl ecparam -out ecdsa_nistp384_sha384_key.pem -name secp384r1 -genkey
|
||||||
|
|
||||||
|
# Convert to pkcs8 format supported by rustls
|
||||||
|
openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp384_sha384_key.pem \
|
||||||
|
-out ecdsa_nistp384_sha384_key_pkcs8.pem
|
||||||
|
|
||||||
|
openssl req -new -nodes -sha384 -key ecdsa_nistp384_sha384_key_pkcs8.pem \
|
||||||
|
-subj "${SUBJECT}" -out server.csr
|
||||||
|
|
||||||
|
openssl x509 -req -sha384 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
|
||||||
|
-CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
|
||||||
|
-in server.csr -out ecdsa_nistp384_sha384_cert.pem
|
||||||
|
|
||||||
|
rm ca_cert.srl server.csr ecdsa_nistp384_sha384_key.pem
|
||||||
|
}
|
||||||
|
|
||||||
|
case $1 in
|
||||||
|
ed25519) gen_ed25519 ;;
|
||||||
|
rsa_sha256) gen_rsa_sha256 ;;
|
||||||
|
ecdsa_nistp256_sha256) gen_ecdsa_nistp256_sha256 ;;
|
||||||
|
ecdsa_nistp384_sha384) gen_ecdsa_nistp384_sha384 ;;
|
||||||
|
*)
|
||||||
|
gen_ed25519
|
||||||
|
gen_rsa_sha256
|
||||||
|
gen_ecdsa_nistp256_sha256
|
||||||
|
gen_ecdsa_nistp384_sha384
|
||||||
|
;;
|
||||||
|
esac
|
30
web/private/rsa_sha256_cert.pem
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
52
web/private/rsa_sha256_key.pem
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
ce4G+zZOOYXwvWGJLwNhgsve8C3oqg==
|
||||||
|
-----END PRIVATE KEY-----
|
4
web/src/consts.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub const DISCORD_OAUTH_TOKEN: &'static str = "https://discord.com/api/oauth2/token";
|
||||||
|
pub const DISCORD_OAUTH_AUTHORIZE: &'static str = "https://discord.com/api/oauth2/authorize";
|
||||||
|
pub const DISCORD_API: &'static str = "https://discord.com/api";
|
||||||
|
pub const DISCORD_CDN: &'static str = "https://cdn.discordapp.com/avatars";
|
106
web/src/lib.rs
@ -1,8 +1,102 @@
|
|||||||
#[cfg(test)]
|
#[macro_use]
|
||||||
mod tests {
|
extern crate rocket;
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
mod consts;
|
||||||
let result = 2 + 2;
|
mod routes;
|
||||||
assert_eq!(result, 4);
|
|
||||||
|
use rocket::fs::{relative, FileServer};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use oauth2::basic::BasicClient;
|
||||||
|
use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
|
||||||
|
|
||||||
|
use crate::consts::{DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN};
|
||||||
|
use rocket_dyn_templates::Template;
|
||||||
|
use serenity::client::Context;
|
||||||
|
use sqlx::{MySql, Pool};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
type Database = MySql;
|
||||||
|
|
||||||
|
#[catch(401)]
|
||||||
|
async fn not_authorized() -> Template {
|
||||||
|
let map: HashMap<String, String> = HashMap::new();
|
||||||
|
Template::render("errors/401", &map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[catch(403)]
|
||||||
|
async fn forbidden() -> Template {
|
||||||
|
let map: HashMap<String, String> = HashMap::new();
|
||||||
|
Template::render("errors/403", &map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[catch(404)]
|
||||||
|
async fn not_found() -> Template {
|
||||||
|
let map: HashMap<String, String> = HashMap::new();
|
||||||
|
Template::render("errors/404", &map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[catch(500)]
|
||||||
|
async fn internal_server_error() -> Template {
|
||||||
|
let map: HashMap<String, String> = HashMap::new();
|
||||||
|
Template::render("errors/500", &map)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize(
|
||||||
|
serenity_context: Context,
|
||||||
|
db_pool: Pool<Database>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let oauth2_client = BasicClient::new(
|
||||||
|
ClientId::new(env::var("OAUTH2_CLIENT_ID")?),
|
||||||
|
Some(ClientSecret::new(env::var("OAUTH2_CLIENT_SECRET")?)),
|
||||||
|
AuthUrl::new(DISCORD_OAUTH_AUTHORIZE.to_string())?,
|
||||||
|
Some(TokenUrl::new(DISCORD_OAUTH_TOKEN.to_string())?),
|
||||||
|
)
|
||||||
|
.set_redirect_uri(RedirectUrl::new(env::var("OAUTH2_DISCORD_CALLBACK")?)?);
|
||||||
|
|
||||||
|
let reqwest_client = reqwest::Client::new();
|
||||||
|
|
||||||
|
rocket::build()
|
||||||
|
.attach(Template::fairing())
|
||||||
|
.register("/", catchers![not_authorized, forbidden, not_found, internal_server_error])
|
||||||
|
.manage(oauth2_client)
|
||||||
|
.manage(reqwest_client)
|
||||||
|
.manage(serenity_context)
|
||||||
|
.manage(db_pool)
|
||||||
|
.mount("/static", FileServer::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static")))
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
routes![
|
||||||
|
routes::index,
|
||||||
|
routes::cookies,
|
||||||
|
routes::privacy,
|
||||||
|
routes::terms,
|
||||||
|
routes::help,
|
||||||
|
routes::return_to_same_site
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.mount("/login", routes![routes::login::discord_login, routes::login::discord_callback])
|
||||||
|
.mount(
|
||||||
|
"/dashboard",
|
||||||
|
routes![
|
||||||
|
routes::dashboard::dashboard_home,
|
||||||
|
routes::dashboard::user::get_user_info,
|
||||||
|
routes::dashboard::user::update_user_info,
|
||||||
|
routes::dashboard::user::get_user_guilds,
|
||||||
|
routes::dashboard::user::create_reminder,
|
||||||
|
routes::dashboard::user::get_reminders,
|
||||||
|
routes::dashboard::user::overwrite_reminder,
|
||||||
|
routes::dashboard::user::delete_reminder,
|
||||||
|
routes::dashboard::guild::get_guild_channels,
|
||||||
|
routes::dashboard::guild::get_guild_roles,
|
||||||
|
routes::dashboard::guild::create_reminder,
|
||||||
|
routes::dashboard::guild::get_reminders,
|
||||||
|
routes::dashboard::guild::edit_reminder,
|
||||||
|
routes::dashboard::guild::delete_reminder,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.launch()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
205
web/src/routes/dashboard/guild.rs
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
use rocket::State;
|
||||||
|
|
||||||
|
use crate::consts::DISCORD_CDN;
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::{MySql, Pool};
|
||||||
|
|
||||||
|
use super::Reminder;
|
||||||
|
use rocket::serde::json::{json, Json, Value as JsonValue};
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::http::CacheHttp;
|
||||||
|
use serenity::model::id::GuildId;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ChannelInfo {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
webhook_avatar: Option<String>,
|
||||||
|
webhook_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo check the user can access this guild
|
||||||
|
#[get("/api/guild/<id>/channels")]
|
||||||
|
pub async fn get_guild_channels(
|
||||||
|
id: u64,
|
||||||
|
ctx: &State<Context>,
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
) -> JsonValue {
|
||||||
|
let channels_res = GuildId(id).channels(ctx.inner()).await;
|
||||||
|
|
||||||
|
match channels_res {
|
||||||
|
Ok(channels) => {
|
||||||
|
let mut channel_info = vec![];
|
||||||
|
|
||||||
|
for (channel_id, channel) in
|
||||||
|
channels.iter().filter(|(_, channel)| channel.is_text_based())
|
||||||
|
{
|
||||||
|
let mut ch = ChannelInfo {
|
||||||
|
name: channel.name.to_string(),
|
||||||
|
id: channel_id.to_string(),
|
||||||
|
webhook_avatar: None,
|
||||||
|
webhook_name: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(webhook_details) = sqlx::query!(
|
||||||
|
"SELECT webhook_id, webhook_token FROM channels WHERE channel = ?",
|
||||||
|
channel.id.as_u64()
|
||||||
|
)
|
||||||
|
.fetch_one(pool.inner())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if let (Some(webhook_id), Some(webhook_token)) =
|
||||||
|
(webhook_details.webhook_id, webhook_details.webhook_token)
|
||||||
|
{
|
||||||
|
let webhook_res =
|
||||||
|
ctx.http.get_webhook_with_token(webhook_id, &webhook_token).await;
|
||||||
|
|
||||||
|
if let Ok(webhook) = webhook_res {
|
||||||
|
ch.webhook_avatar = webhook.avatar.map(|a| {
|
||||||
|
format!("{}/{}/{}.webp?size=128", DISCORD_CDN, webhook_id, a)
|
||||||
|
});
|
||||||
|
|
||||||
|
ch.webhook_name = webhook.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_info.push(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
json!(channel_info)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Could not fetch channels from {}: {:?}", id, e);
|
||||||
|
|
||||||
|
json!({"error": "Could not get channels"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct RoleInfo {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo check the user can access this guild
|
||||||
|
#[get("/api/guild/<id>/roles")]
|
||||||
|
pub async fn get_guild_roles(id: u64, ctx: &State<Context>) -> JsonValue {
|
||||||
|
let roles_res = ctx.cache.guild_roles(id);
|
||||||
|
|
||||||
|
match roles_res {
|
||||||
|
Some(roles) => {
|
||||||
|
let roles = roles
|
||||||
|
.iter()
|
||||||
|
.map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
|
||||||
|
.collect::<Vec<RoleInfo>>();
|
||||||
|
|
||||||
|
json!(roles)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("Could not fetch roles from {}", id);
|
||||||
|
|
||||||
|
json!({"error": "Could not get roles"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||||
|
pub async fn create_reminder(
|
||||||
|
id: u64,
|
||||||
|
reminder: Json<Reminder>,
|
||||||
|
serenity_context: &State<Context>,
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
) -> JsonValue {
|
||||||
|
json!({"error": "Not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/guild/<id>/reminders")]
|
||||||
|
pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySql>>) -> JsonValue {
|
||||||
|
let channels_res = GuildId(id).channels(&ctx.inner()).await;
|
||||||
|
|
||||||
|
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!(
|
||||||
|
Reminder,
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
reminders.attachment,
|
||||||
|
reminders.attachment_name,
|
||||||
|
reminders.avatar,
|
||||||
|
channels.channel,
|
||||||
|
reminders.content,
|
||||||
|
reminders.embed_author,
|
||||||
|
reminders.embed_author_url,
|
||||||
|
reminders.embed_color,
|
||||||
|
reminders.embed_description,
|
||||||
|
reminders.embed_footer,
|
||||||
|
reminders.embed_footer_url,
|
||||||
|
reminders.embed_image_url,
|
||||||
|
reminders.embed_thumbnail_url,
|
||||||
|
reminders.embed_title,
|
||||||
|
reminders.enabled,
|
||||||
|
reminders.expires,
|
||||||
|
reminders.interval_seconds,
|
||||||
|
reminders.interval_months,
|
||||||
|
reminders.name,
|
||||||
|
reminders.pin,
|
||||||
|
reminders.restartable,
|
||||||
|
reminders.tts,
|
||||||
|
reminders.uid,
|
||||||
|
reminders.username,
|
||||||
|
reminders.utc_time
|
||||||
|
FROM
|
||||||
|
reminders
|
||||||
|
LEFT JOIN
|
||||||
|
channels
|
||||||
|
ON
|
||||||
|
channels.id = reminders.channel_id
|
||||||
|
WHERE
|
||||||
|
FIND_IN_SET(channels.channel, ?)
|
||||||
|
",
|
||||||
|
channels
|
||||||
|
)
|
||||||
|
.fetch_all(pool.inner())
|
||||||
|
.await
|
||||||
|
.map(|r| json!(r))
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
warn!("Failed to complete SQL query: {:?}", e);
|
||||||
|
|
||||||
|
json!({"error": "Could not load reminders"})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Could not fetch channels from {}: {:?}", id, e);
|
||||||
|
|
||||||
|
json!([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||||
|
pub async fn edit_reminder(
|
||||||
|
id: u64,
|
||||||
|
reminder: Json<Reminder>,
|
||||||
|
serenity_context: &State<Context>,
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
) -> JsonValue {
|
||||||
|
json!({"error": "Not implemented"})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/api/guild/<id>/reminders", data = "<reminder>")]
|
||||||
|
pub async fn delete_reminder(
|
||||||
|
id: u64,
|
||||||
|
reminder: Json<Reminder>,
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
) -> JsonValue {
|
||||||
|
json!({"error": "Not implemented"})
|
||||||
|
}
|
59
web/src/routes/dashboard/mod.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use chrono::naive::NaiveDateTime;
|
||||||
|
use rocket::http::CookieJar;
|
||||||
|
use rocket::response::Redirect;
|
||||||
|
use rocket_dyn_templates::Template;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub mod guild;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
|
fn name_default() -> String {
|
||||||
|
"Reminder".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Reminder {
|
||||||
|
attachment: Option<Vec<u8>>,
|
||||||
|
attachment_name: Option<String>,
|
||||||
|
avatar: Option<String>,
|
||||||
|
channel: u64,
|
||||||
|
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,
|
||||||
|
enabled: i8,
|
||||||
|
expires: Option<NaiveDateTime>,
|
||||||
|
interval_seconds: Option<u32>,
|
||||||
|
interval_months: Option<u32>,
|
||||||
|
#[serde(default = "name_default")]
|
||||||
|
name: String,
|
||||||
|
pin: i8,
|
||||||
|
restartable: i8,
|
||||||
|
tts: i8,
|
||||||
|
#[serde(default)]
|
||||||
|
uid: String,
|
||||||
|
username: Option<String>,
|
||||||
|
utc_time: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct DeleteReminder {
|
||||||
|
uid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn dashboard_home(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"))
|
||||||
|
}
|
||||||
|
}
|
402
web/src/routes/dashboard/user.rs
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
use rocket::serde::json::{json, Json, Value as JsonValue};
|
||||||
|
use rocket::{http::CookieJar, State};
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serenity::model::{
|
||||||
|
id::{GuildId, RoleId},
|
||||||
|
permissions::Permissions,
|
||||||
|
};
|
||||||
|
use sqlx::{MySql, Pool};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use super::Reminder;
|
||||||
|
use crate::consts::DISCORD_API;
|
||||||
|
use crate::routes::dashboard::DeleteReminder;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::model::id::UserId;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct UserInfo {
|
||||||
|
name: String,
|
||||||
|
patreon: bool,
|
||||||
|
timezone: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateUser {
|
||||||
|
timezone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct GuildInfo {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PartialGuild {
|
||||||
|
pub id: GuildId,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub owner: bool,
|
||||||
|
#[serde(rename = "permissions_new")]
|
||||||
|
pub permissions: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/user")]
|
||||||
|
pub async fn get_user_info(
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
ctx: &State<Context>,
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
) -> JsonValue {
|
||||||
|
if let Some(user_id) =
|
||||||
|
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
|
||||||
|
{
|
||||||
|
let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
|
||||||
|
.member(&ctx.inner(), user_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let timezone = sqlx::query!("SELECT timezone FROM users WHERE user = ?", user_id)
|
||||||
|
.fetch_one(pool.inner())
|
||||||
|
.await
|
||||||
|
.map_or(None, |q| Some(q.timezone));
|
||||||
|
|
||||||
|
let user_info = UserInfo {
|
||||||
|
name: cookies
|
||||||
|
.get_private("username")
|
||||||
|
.map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()),
|
||||||
|
patreon: member_res.map_or(false, |member| {
|
||||||
|
member
|
||||||
|
.roles
|
||||||
|
.contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
|
||||||
|
}),
|
||||||
|
timezone,
|
||||||
|
};
|
||||||
|
|
||||||
|
json!(user_info)
|
||||||
|
} else {
|
||||||
|
json!({"error": "Not authorized"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("/api/user", data = "<user>")]
|
||||||
|
pub async fn update_user_info(
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
user: Json<UpdateUser>,
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
) -> JsonValue {
|
||||||
|
if let Some(user_id) =
|
||||||
|
cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
|
||||||
|
{
|
||||||
|
if user.timezone.parse::<Tz>().is_ok() {
|
||||||
|
let _ = sqlx::query!(
|
||||||
|
"UPDATE users SET timezone = ? WHERE user = ?",
|
||||||
|
user.timezone,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
.execute(pool.inner())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
json!({})
|
||||||
|
} else {
|
||||||
|
json!({"error": "Timezone not recognized"})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
json!({"error": "Not authorized"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/user/guilds")]
|
||||||
|
pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue {
|
||||||
|
if let Some(access_token) = cookies.get_private("access_token") {
|
||||||
|
let request_res = reqwest_client
|
||||||
|
.get(format!("{}/users/@me/guilds", DISCORD_API))
|
||||||
|
.bearer_auth(access_token.value())
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match request_res {
|
||||||
|
Ok(response) => {
|
||||||
|
let guilds_res = response.json::<Vec<PartialGuild>>().await;
|
||||||
|
|
||||||
|
match guilds_res {
|
||||||
|
Ok(guilds) => {
|
||||||
|
let reduced_guilds = guilds
|
||||||
|
.iter()
|
||||||
|
.filter(|g| {
|
||||||
|
g.owner
|
||||||
|
|| g.permissions.as_ref().map_or(false, |p| {
|
||||||
|
let permissions =
|
||||||
|
Permissions::from_bits_truncate(p.parse().unwrap());
|
||||||
|
|
||||||
|
permissions.manage_messages()
|
||||||
|
|| permissions.manage_guild()
|
||||||
|
|| permissions.administrator()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() })
|
||||||
|
.collect::<Vec<GuildInfo>>();
|
||||||
|
|
||||||
|
json!(reduced_guilds)
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error constructing user from request: {:?}", e);
|
||||||
|
|
||||||
|
json!({"error": "Could not get user details"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error getting user guilds: {:?}", e);
|
||||||
|
|
||||||
|
json!({"error": "Could not reach Discord"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
json!({"error": "Not authorized"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/user/reminders", data = "<reminder>")]
|
||||||
|
pub async fn create_reminder(
|
||||||
|
reminder: Json<Reminder>,
|
||||||
|
ctx: &State<Context>,
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
) -> JsonValue {
|
||||||
|
match sqlx::query!(
|
||||||
|
"INSERT INTO reminders (
|
||||||
|
avatar,
|
||||||
|
content,
|
||||||
|
embed_author,
|
||||||
|
embed_author_url,
|
||||||
|
embed_color,
|
||||||
|
embed_description,
|
||||||
|
embed_footer,
|
||||||
|
embed_footer_url,
|
||||||
|
embed_image_url,
|
||||||
|
embed_thumbnail_url,
|
||||||
|
embed_title,
|
||||||
|
enabled,
|
||||||
|
expires,
|
||||||
|
interval_seconds,
|
||||||
|
interval_months,
|
||||||
|
name,
|
||||||
|
pin,
|
||||||
|
restartable,
|
||||||
|
tts,
|
||||||
|
username,
|
||||||
|
`utc_time`
|
||||||
|
) VALUES (
|
||||||
|
avatar = ?,
|
||||||
|
content = ?,
|
||||||
|
embed_author = ?,
|
||||||
|
embed_author_url = ?,
|
||||||
|
embed_color = ?,
|
||||||
|
embed_description = ?,
|
||||||
|
embed_footer = ?,
|
||||||
|
embed_footer_url = ?,
|
||||||
|
embed_image_url = ?,
|
||||||
|
embed_thumbnail_url = ?,
|
||||||
|
embed_title = ?,
|
||||||
|
enabled = ?,
|
||||||
|
expires = ?,
|
||||||
|
interval_seconds = ?,
|
||||||
|
interval_months = ?,
|
||||||
|
name = ?,
|
||||||
|
pin = ?,
|
||||||
|
restartable = ?,
|
||||||
|
tts = ?,
|
||||||
|
username = ?,
|
||||||
|
`utc_time` = ?
|
||||||
|
)",
|
||||||
|
reminder.avatar,
|
||||||
|
reminder.content,
|
||||||
|
reminder.embed_author,
|
||||||
|
reminder.embed_author_url,
|
||||||
|
reminder.embed_color,
|
||||||
|
reminder.embed_description,
|
||||||
|
reminder.embed_footer,
|
||||||
|
reminder.embed_footer_url,
|
||||||
|
reminder.embed_image_url,
|
||||||
|
reminder.embed_thumbnail_url,
|
||||||
|
reminder.embed_title,
|
||||||
|
reminder.enabled,
|
||||||
|
reminder.expires,
|
||||||
|
reminder.interval_seconds,
|
||||||
|
reminder.interval_months,
|
||||||
|
reminder.name,
|
||||||
|
reminder.pin,
|
||||||
|
reminder.restartable,
|
||||||
|
reminder.tts,
|
||||||
|
reminder.username,
|
||||||
|
reminder.utc_time,
|
||||||
|
)
|
||||||
|
.execute(pool.inner())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
json!({})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error in `create_reminder`: {:?}", e);
|
||||||
|
|
||||||
|
json!({"error": "Could not create reminder"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/user/reminders")]
|
||||||
|
pub async fn get_reminders(
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
ctx: &State<Context>,
|
||||||
|
) -> JsonValue {
|
||||||
|
if let Some(user_id) =
|
||||||
|
cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten()
|
||||||
|
{
|
||||||
|
let query_res = sqlx::query!(
|
||||||
|
"SELECT channel FROM channels INNER JOIN users ON users.dm_channel = channels.id WHERE users.user = ?",
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_one(pool.inner())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let dm_channel = if let Ok(query) = query_res {
|
||||||
|
Some(query.channel)
|
||||||
|
} else {
|
||||||
|
if let Ok(dm_channel) = UserId(user_id).create_dm_channel(&ctx.inner()).await {
|
||||||
|
Some(dm_channel.id.as_u64().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(channel_id) = dm_channel {
|
||||||
|
let reminders = sqlx::query_as!(
|
||||||
|
Reminder,
|
||||||
|
"SELECT
|
||||||
|
reminders.attachment,
|
||||||
|
reminders.attachment_name,
|
||||||
|
reminders.avatar,
|
||||||
|
channels.channel,
|
||||||
|
reminders.content,
|
||||||
|
reminders.embed_author,
|
||||||
|
reminders.embed_author_url,
|
||||||
|
reminders.embed_color,
|
||||||
|
reminders.embed_description,
|
||||||
|
reminders.embed_footer,
|
||||||
|
reminders.embed_footer_url,
|
||||||
|
reminders.embed_image_url,
|
||||||
|
reminders.embed_thumbnail_url,
|
||||||
|
reminders.embed_title,
|
||||||
|
reminders.enabled,
|
||||||
|
reminders.expires,
|
||||||
|
reminders.interval_seconds,
|
||||||
|
reminders.interval_months,
|
||||||
|
reminders.name,
|
||||||
|
reminders.pin,
|
||||||
|
reminders.restartable,
|
||||||
|
reminders.tts,
|
||||||
|
reminders.uid,
|
||||||
|
reminders.username,
|
||||||
|
reminders.utc_time
|
||||||
|
FROM reminders INNER JOIN channels ON channels.id = reminders.channel_id WHERE channels.channel = ?",
|
||||||
|
channel_id
|
||||||
|
)
|
||||||
|
.fetch_all(pool.inner())
|
||||||
|
.await
|
||||||
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
json!(reminders)
|
||||||
|
} else {
|
||||||
|
json!({"error": "User's DM channel could not be determined"})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
json!({"error": "Not authorized"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/api/user/reminders", data = "<reminder>")]
|
||||||
|
pub async fn overwrite_reminder(reminder: Json<Reminder>, pool: &State<Pool<MySql>>) -> JsonValue {
|
||||||
|
match sqlx::query!(
|
||||||
|
"UPDATE reminders SET
|
||||||
|
avatar = ?,
|
||||||
|
content = ?,
|
||||||
|
embed_author = ?,
|
||||||
|
embed_author_url = ?,
|
||||||
|
embed_color = ?,
|
||||||
|
embed_description = ?,
|
||||||
|
embed_footer = ?,
|
||||||
|
embed_footer_url = ?,
|
||||||
|
embed_image_url = ?,
|
||||||
|
embed_thumbnail_url = ?,
|
||||||
|
embed_title = ?,
|
||||||
|
enabled = ?,
|
||||||
|
expires = ?,
|
||||||
|
interval_seconds = ?,
|
||||||
|
interval_months = ?,
|
||||||
|
name = ?,
|
||||||
|
pin = ?,
|
||||||
|
restartable = ?,
|
||||||
|
tts = ?,
|
||||||
|
username = ?,
|
||||||
|
`utc_time` = ?
|
||||||
|
WHERE uid = ?",
|
||||||
|
reminder.avatar,
|
||||||
|
reminder.content,
|
||||||
|
reminder.embed_author,
|
||||||
|
reminder.embed_author_url,
|
||||||
|
reminder.embed_color,
|
||||||
|
reminder.embed_description,
|
||||||
|
reminder.embed_footer,
|
||||||
|
reminder.embed_footer_url,
|
||||||
|
reminder.embed_image_url,
|
||||||
|
reminder.embed_thumbnail_url,
|
||||||
|
reminder.embed_title,
|
||||||
|
reminder.enabled,
|
||||||
|
reminder.expires,
|
||||||
|
reminder.interval_seconds,
|
||||||
|
reminder.interval_months,
|
||||||
|
reminder.name,
|
||||||
|
reminder.pin,
|
||||||
|
reminder.restartable,
|
||||||
|
reminder.tts,
|
||||||
|
reminder.username,
|
||||||
|
reminder.utc_time,
|
||||||
|
reminder.uid
|
||||||
|
)
|
||||||
|
.execute(pool.inner())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
json!({})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error in `overwrite_reminder`: {:?}", e);
|
||||||
|
|
||||||
|
json!({"error": "Could not modify reminder"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/api/user/reminders", data = "<reminder>")]
|
||||||
|
pub async fn delete_reminder(
|
||||||
|
reminder: Json<DeleteReminder>,
|
||||||
|
pool: &State<Pool<MySql>>,
|
||||||
|
) -> JsonValue {
|
||||||
|
if sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid)
|
||||||
|
.execute(pool.inner())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
json!({})
|
||||||
|
} else {
|
||||||
|
json!({"error": "Could not delete reminder"})
|
||||||
|
}
|
||||||
|
}
|
149
web/src/routes/login.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
use crate::consts::DISCORD_API;
|
||||||
|
use log::warn;
|
||||||
|
use oauth2::basic::BasicClient;
|
||||||
|
use oauth2::reqwest::async_http_client;
|
||||||
|
use oauth2::{
|
||||||
|
AuthorizationCode, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
|
||||||
|
};
|
||||||
|
use reqwest::Client;
|
||||||
|
use rocket::http::private::cookie::Expiration;
|
||||||
|
use rocket::http::{Cookie, CookieJar, SameSite};
|
||||||
|
use rocket::response::{Flash, Redirect};
|
||||||
|
use rocket::uri;
|
||||||
|
use rocket::State;
|
||||||
|
use serenity::model::user::User;
|
||||||
|
|
||||||
|
#[get("/discord")]
|
||||||
|
pub async fn discord_login(
|
||||||
|
oauth2_client: &State<BasicClient>,
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
) -> Redirect {
|
||||||
|
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
|
||||||
|
let (auth_url, csrf_token) = oauth2_client
|
||||||
|
.authorize_url(CsrfToken::new_random)
|
||||||
|
// Set the desired scopes.
|
||||||
|
.add_scope(Scope::new("identify".to_string()))
|
||||||
|
.add_scope(Scope::new("guilds".to_string()))
|
||||||
|
.add_scope(Scope::new("email".to_string()))
|
||||||
|
// Set the PKCE code challenge.
|
||||||
|
.set_pkce_challenge(pkce_challenge)
|
||||||
|
.url();
|
||||||
|
|
||||||
|
// store the pkce secret to verify the authorization later
|
||||||
|
cookies.add_private(
|
||||||
|
Cookie::build("verify", pkce_verifier.secret().to_string())
|
||||||
|
.http_only(true)
|
||||||
|
.path("/login")
|
||||||
|
.same_site(SameSite::Lax)
|
||||||
|
.expires(Expiration::Session)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// store the csrf token to verify no interference
|
||||||
|
cookies.add_private(
|
||||||
|
Cookie::build("csrf", csrf_token.secret().to_string())
|
||||||
|
.http_only(true)
|
||||||
|
.path("/login")
|
||||||
|
.same_site(SameSite::Lax)
|
||||||
|
.expires(Expiration::Session)
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Redirect::to(auth_url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/discord/authorized?<code>&<state>")]
|
||||||
|
pub async fn discord_callback(
|
||||||
|
code: &str,
|
||||||
|
state: &str,
|
||||||
|
cookies: &CookieJar<'_>,
|
||||||
|
oauth2_client: &State<BasicClient>,
|
||||||
|
reqwest_client: &State<Client>,
|
||||||
|
) -> Result<Redirect, Flash<Redirect>> {
|
||||||
|
if let (Some(pkce_secret), Some(csrf_token)) =
|
||||||
|
(cookies.get_private("verify"), cookies.get_private("csrf"))
|
||||||
|
{
|
||||||
|
if state == csrf_token.value() {
|
||||||
|
let token_result = oauth2_client
|
||||||
|
.exchange_code(AuthorizationCode::new(code.to_string()))
|
||||||
|
// Set the PKCE code verifier.
|
||||||
|
.set_pkce_verifier(PkceCodeVerifier::new(pkce_secret.value().to_string()))
|
||||||
|
.request_async(async_http_client)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cookies.remove_private(Cookie::named("verify"));
|
||||||
|
cookies.remove_private(Cookie::named("csrf"));
|
||||||
|
|
||||||
|
match token_result {
|
||||||
|
Ok(token) => {
|
||||||
|
cookies.add_private(
|
||||||
|
Cookie::build("access_token", token.access_token().secret().to_string())
|
||||||
|
.secure(true)
|
||||||
|
.http_only(true)
|
||||||
|
.path("/dashboard")
|
||||||
|
.finish(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let request_res = reqwest_client
|
||||||
|
.get(format!("{}/users/@me", DISCORD_API))
|
||||||
|
.bearer_auth(token.access_token().secret())
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match request_res {
|
||||||
|
Ok(response) => {
|
||||||
|
let user_res = response.json::<User>().await;
|
||||||
|
|
||||||
|
match user_res {
|
||||||
|
Ok(user) => {
|
||||||
|
let user_name = format!("{}#{}", user.name, user.discriminator);
|
||||||
|
let user_id = user.id.as_u64().to_string();
|
||||||
|
|
||||||
|
cookies.add_private(Cookie::new("username", user_name));
|
||||||
|
cookies.add_private(Cookie::new("userid", user_id));
|
||||||
|
|
||||||
|
Ok(Redirect::to(uri!(super::return_to_same_site("dashboard"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error constructing user from request: {:?}", e);
|
||||||
|
|
||||||
|
Err(Flash::new(
|
||||||
|
Redirect::to(uri!(super::return_to_same_site(""))),
|
||||||
|
"danger",
|
||||||
|
"Failed to contact Discord",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error getting user info: {:?}", e);
|
||||||
|
|
||||||
|
Err(Flash::new(
|
||||||
|
Redirect::to(uri!(super::return_to_same_site(""))),
|
||||||
|
"danger",
|
||||||
|
"Failed to contact Discord",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Error in discord callback: {:?}", e);
|
||||||
|
|
||||||
|
Err(Flash::new(
|
||||||
|
Redirect::to(uri!(super::return_to_same_site(""))),
|
||||||
|
"warning",
|
||||||
|
"Your login request was rejected",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "danger", "Your request failed to validate, and so has been rejected (error: CSRF Validation Failure)"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "warning", "Your request was missing information, and so has been rejected (error: CSRF Validation Tokens Missing)"))
|
||||||
|
}
|
||||||
|
}
|
51
web/src/routes/mod.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
pub mod dashboard;
|
||||||
|
pub mod login;
|
||||||
|
|
||||||
|
use rocket::request::FlashMessage;
|
||||||
|
use rocket_dyn_templates::Template;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn index(flash: Option<FlashMessage<'_>>) -> Template {
|
||||||
|
let mut map: HashMap<&str, String> = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(message) = flash {
|
||||||
|
map.insert("flashed_message", message.message().to_string());
|
||||||
|
map.insert("flashed_grade", message.kind().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Template::render("index", &map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/ret?<to>")]
|
||||||
|
pub async fn return_to_same_site(to: &str) -> Template {
|
||||||
|
let mut map: HashMap<&str, String> = HashMap::new();
|
||||||
|
|
||||||
|
map.insert("to", to.to_string());
|
||||||
|
|
||||||
|
Template::render("return", &map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/cookies")]
|
||||||
|
pub async fn cookies() -> Template {
|
||||||
|
let map: HashMap<&str, String> = HashMap::new();
|
||||||
|
Template::render("cookies", &map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/privacy")]
|
||||||
|
pub async fn privacy() -> Template {
|
||||||
|
let map: HashMap<&str, String> = HashMap::new();
|
||||||
|
Template::render("privacy", &map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/terms")]
|
||||||
|
pub async fn terms() -> Template {
|
||||||
|
let map: HashMap<&str, String> = HashMap::new();
|
||||||
|
Template::render("terms", &map)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/help")]
|
||||||
|
pub async fn help() -> Template {
|
||||||
|
let map: HashMap<&str, String> = HashMap::new();
|
||||||
|
Template::render("help", &map)
|
||||||
|
}
|
1
web/static/css/bulma.min.css
vendored
Normal file
91
web/static/css/dtsel.css
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
.date-selector-wrapper {
|
||||||
|
width: 200px;
|
||||||
|
padding: 3px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 1px 1px 10px 1px #5c5c5c;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 12px;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-o-user-select: none;
|
||||||
|
/* user-select: none; */
|
||||||
|
}
|
||||||
|
.cal-header, .cal-row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.cal-cell, .cal-nav {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.cal-day-names {
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
.cal-day-names .cal-cell {
|
||||||
|
cursor: default;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.cal-cell-prev, .cal-cell-next {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.cal-months .cal-row, .cal-years .cal-row {
|
||||||
|
height: 60px;
|
||||||
|
line-height: 60px;
|
||||||
|
}
|
||||||
|
.cal-nav-prev, .cal-nav-next {
|
||||||
|
flex: 0.15;
|
||||||
|
}
|
||||||
|
.cal-nav-current {
|
||||||
|
flex: 0.75;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.cal-months .cal-cell, .cal-years .cal-cell {
|
||||||
|
flex: 0.25;
|
||||||
|
}
|
||||||
|
.cal-days .cal-cell {
|
||||||
|
flex: 0.143;
|
||||||
|
}
|
||||||
|
.cal-value {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #286090;
|
||||||
|
}
|
||||||
|
.cal-cell:hover, .cal-nav:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
.cal-value:hover {
|
||||||
|
background-color: #204d74;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* time footer */
|
||||||
|
.cal-time {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
height: 27px;
|
||||||
|
line-height: 27px;
|
||||||
|
}
|
||||||
|
.cal-time-label, .cal-time-value {
|
||||||
|
flex: 0.12;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.cal-time-slider {
|
||||||
|
flex: 0.77;
|
||||||
|
background-image: linear-gradient(to right, #d1d8dd, #d1d8dd);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 1px;
|
||||||
|
background-position: left 50%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.cal-time-slider input {
|
||||||
|
width: 100%;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
outline: 0;
|
||||||
|
user-select: auto;
|
||||||
|
}
|
12749
web/static/css/fa.css
Normal file
54
web/static/css/font.css
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
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-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-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-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-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-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-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-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgo6eA.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvTtw.ttf) format('truetype');
|
||||||
|
}
|
383
web/static/css/style.css
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
* {
|
||||||
|
font-family: "Ubuntu Bold", "Ubuntu", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.reminderContent {
|
||||||
|
padding: 2px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-pad {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.inset-content {
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-right: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.flash-message {
|
||||||
|
position: fixed;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
margin: 16px !important;
|
||||||
|
z-index: 99;
|
||||||
|
bottom: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.flash-message.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.spacer {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .dashboard-button {
|
||||||
|
background: white ;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.patreon-color {
|
||||||
|
color: #f96854;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#pageNavbar {
|
||||||
|
background-color: #363636;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#pageNavbar a {
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#pageNavbar a:hover {
|
||||||
|
background-color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.rounded-corners {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.brand {
|
||||||
|
text-align: center;
|
||||||
|
height: 48px;
|
||||||
|
background-color: #8fb677;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.dashboard-brand {
|
||||||
|
text-align: center;
|
||||||
|
height: 100%;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dashboard-sidebar {
|
||||||
|
background-color: #363636;
|
||||||
|
width: 230px !important;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.mobile-sidebar {
|
||||||
|
z-index: 100;
|
||||||
|
height: 100vh;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.mobile-sidebar.is-active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dashboard-frame {
|
||||||
|
min-height: 100vh;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu .menu-label {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-navbar {
|
||||||
|
background-color: #8fb677 !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.autoresize {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea, input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input:placeholder-shown {
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom-style: dashed;
|
||||||
|
background-color: #40444b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
border: none;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-input {
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
background-color: #40444b;
|
||||||
|
color: #fff;
|
||||||
|
width: 120px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.message-input::placeholder {
|
||||||
|
color: #72767b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 4px 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-description {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-username {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-message-header {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-content {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable img {
|
||||||
|
background-color: #72767b;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable.is-20x20 img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable.is-24x24 img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable.is-400x300 img {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable.is-32x32 img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable.thumbnail img {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable input.imageInput {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 36px;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable.thumbnail input.imageInput {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -400px;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customizable input.is-active {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-frame {
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #36393f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-embed {
|
||||||
|
padding: 8px 16px 16px 12px;
|
||||||
|
margin: 0 20px 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #fff;
|
||||||
|
background-color: #2f3136;
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-author-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-author-box > .a {
|
||||||
|
flex: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-author-box > .b {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-footer-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-author-box .image {
|
||||||
|
margin: 0 8px 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-footer-box .image {
|
||||||
|
margin: 0 8px 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-embed-author {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-embed-footer {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-body {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-body > .a {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-body input, .embed-body textarea {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-body > .b {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-field-title, .discord-field-value {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-field-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-field-box {
|
||||||
|
margin: 12px 8px 0 0;
|
||||||
|
max-width: 120px;
|
||||||
|
flex: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-input {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-multifield-box {
|
||||||
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-toggle {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-toggle {
|
||||||
|
color: #fcb620;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.level-item.preview-toggle:hover {
|
||||||
|
color: #fcb620;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-toggle.is-active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.level-item.icon-toggle:hover {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.level-item:hover {
|
||||||
|
color: rgb(55, 127, 242)
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-toggle.is-active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-select {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-left: 48px;
|
||||||
|
display: inline-flex;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6e89da;
|
||||||
|
width: auto;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-bottom: 1px solid #fff;
|
||||||
|
}
|
BIN
web/static/favicon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
web/static/favicon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
web/static/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
9
web/static/favicon/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#da532c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
BIN
web/static/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
web/static/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
web/static/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
web/static/favicon/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
19
web/static/favicon/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
BIN
web/static/img/icon.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
web/static/img/logo_flat.jpg
Normal file
After Width: | Height: | Size: 323 KiB |
BIN
web/static/img/slash-commands.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
web/static/img/tournament-demo.png
Normal file
After Width: | Height: | Size: 65 KiB |
931
web/static/js/dtsel.js
Normal file
@ -0,0 +1,931 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var BODYTYPES = ["DAYS", "MONTHS", "YEARS"];
|
||||||
|
var MONTHS = [
|
||||||
|
"January", "February", "March", "April", "May", "June",
|
||||||
|
"July", "August", "September", "October", "November", "December"
|
||||||
|
];
|
||||||
|
var WEEKDAYS = [
|
||||||
|
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @typedef {Object.<string, Function[]>} Handlers */
|
||||||
|
/** @typedef {function(String, Function): null} AddHandler */
|
||||||
|
/** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */
|
||||||
|
/** @typedef {string|number} StringNum */
|
||||||
|
/** @typedef {Object.<string, StringNum>} StringNumObj */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local state
|
||||||
|
* @typedef {Object} InstanceState
|
||||||
|
* @property {Date} value
|
||||||
|
* @property {Number} year
|
||||||
|
* @property {Number} month
|
||||||
|
* @property {Number} day
|
||||||
|
* @property {Number} time
|
||||||
|
* @property {Number} hours
|
||||||
|
* @property {Number} minutes
|
||||||
|
* @property {Number} seconds
|
||||||
|
* @property {BodyType} bodyType
|
||||||
|
* @property {Boolean} visible
|
||||||
|
* @property {Number} cancelBlur
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Config
|
||||||
|
* @property {String} dateFormat
|
||||||
|
* @property {String} timeFormat
|
||||||
|
* @property {Boolean} showDate
|
||||||
|
* @property {Boolean} showTime
|
||||||
|
* @property {Number} paddingX
|
||||||
|
* @property {Number} paddingY
|
||||||
|
* @property {BodyType} defaultView
|
||||||
|
* @property {"TOP"|"BOTTOM"} direction
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @param {HTMLElement} elem
|
||||||
|
* @param {Config} config
|
||||||
|
*/
|
||||||
|
function DTS(elem, config) {
|
||||||
|
var config = config || {};
|
||||||
|
|
||||||
|
/** @type {Config} */
|
||||||
|
var defaultConfig = {
|
||||||
|
defaultView: BODYTYPES[0],
|
||||||
|
dateFormat: "yyyy-mm-dd",
|
||||||
|
timeFormat: "HH:MM:SS",
|
||||||
|
showDate: true,
|
||||||
|
showTime: false,
|
||||||
|
paddingX: 5,
|
||||||
|
paddingY: 5,
|
||||||
|
direction: 'TOP'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!elem) {
|
||||||
|
throw TypeError("input element or selector required for contructor");
|
||||||
|
}
|
||||||
|
if (Object.getPrototypeOf(elem) === String.prototype) {
|
||||||
|
var _elem = document.querySelectorAll(elem);
|
||||||
|
if (!_elem[0]){
|
||||||
|
throw Error('"' + elem + '" not found.');
|
||||||
|
}
|
||||||
|
elem = _elem[0];
|
||||||
|
}
|
||||||
|
this.config = setDefaults(config, defaultConfig);
|
||||||
|
this.dateFormat = this.config.dateFormat;
|
||||||
|
this.timeFormat = this.config.timeFormat;
|
||||||
|
this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi");
|
||||||
|
this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi");
|
||||||
|
this.inputElem = elem;
|
||||||
|
this.dtbox = null;
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
DTS.prototype.setup = function () {
|
||||||
|
var handler = this.inputElemHandler.bind(this);
|
||||||
|
this.inputElem.addEventListener("focus", handler, false)
|
||||||
|
this.inputElem.addEventListener("blur", handler, false);
|
||||||
|
}
|
||||||
|
DTS.prototype.inputElemHandler = function (e) {
|
||||||
|
if (e.type == "focus") {
|
||||||
|
if (!this.dtbox) {
|
||||||
|
this.dtbox = new DTBox(e.target, this);
|
||||||
|
}
|
||||||
|
this.dtbox.visible = true;
|
||||||
|
} else if (e.type == "blur" && this.dtbox && this.dtbox.visible) {
|
||||||
|
var self = this;
|
||||||
|
setTimeout(function () {
|
||||||
|
if (self.dtbox.cancelBlur > 0) {
|
||||||
|
self.dtbox.cancelBlur -= 1;
|
||||||
|
} else {
|
||||||
|
self.dtbox.visible = false;
|
||||||
|
self.inputElem.blur();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @param {HTMLElement} elem
|
||||||
|
* @param {DTS} settings
|
||||||
|
*/
|
||||||
|
function DTBox(elem, settings) {
|
||||||
|
/** @type {DTBox} */
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
/** @type {Handlers} */
|
||||||
|
var handlers = {};
|
||||||
|
|
||||||
|
/** @type {InstanceState} */
|
||||||
|
var localState = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} key
|
||||||
|
* @param {*} default_val
|
||||||
|
*/
|
||||||
|
function getterSetter(key, default_val) {
|
||||||
|
return {
|
||||||
|
get: function () {
|
||||||
|
var val = localState[key];
|
||||||
|
return val === undefined ? default_val : val;
|
||||||
|
},
|
||||||
|
set: function (val) {
|
||||||
|
var prevState = self.state;
|
||||||
|
var _handlers = handlers[key] || [];
|
||||||
|
localState[key] = val;
|
||||||
|
for (var i = 0; i < _handlers.length; i++) {
|
||||||
|
_handlers[i].bind(self)(localState, prevState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {AddHandler} */
|
||||||
|
function addHandler(key, handlerFn) {
|
||||||
|
if (!key || !handlerFn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!handlers[key]) {
|
||||||
|
handlers[key] = [];
|
||||||
|
}
|
||||||
|
handlers[key].push(handlerFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperties(this, {
|
||||||
|
visible: getterSetter("visible", false),
|
||||||
|
bodyType: getterSetter("bodyType", settings.config.defaultView),
|
||||||
|
value: getterSetter("value"),
|
||||||
|
year: getterSetter("year", 0),
|
||||||
|
month: getterSetter("month", 0),
|
||||||
|
day: getterSetter("day", 0),
|
||||||
|
hours: getterSetter("hours", 0),
|
||||||
|
minutes: getterSetter("minutes", 0),
|
||||||
|
seconds: getterSetter("seconds", 0),
|
||||||
|
cancelBlur: getterSetter("cancelBlur", 0),
|
||||||
|
addHandler: {value: addHandler},
|
||||||
|
month_long: {
|
||||||
|
get: function () {
|
||||||
|
return MONTHS[self.month];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
month_short: {
|
||||||
|
get: function () {
|
||||||
|
return self.month_long.slice(0, 3);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
get: function () {
|
||||||
|
return Object.assign({}, localState);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
get: function() {
|
||||||
|
var hours = self.hours * 60 * 60 * 1000;
|
||||||
|
var minutes = self.minutes * 60 * 1000;
|
||||||
|
var seconds = self.seconds * 1000;
|
||||||
|
return hours + minutes + seconds;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.el = {};
|
||||||
|
this.settings = settings;
|
||||||
|
this.elem = elem;
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
DTBox.prototype.setup = function () {
|
||||||
|
Object.defineProperties(this.el, {
|
||||||
|
wrapper: { value: null, configurable: true },
|
||||||
|
header: { value: null, configurable: true },
|
||||||
|
body: { value: null, configurable: true },
|
||||||
|
footer: { value: null, configurable: true }
|
||||||
|
});
|
||||||
|
this.setupWrapper();
|
||||||
|
if (this.settings.config.showDate) {
|
||||||
|
this.setupHeader();
|
||||||
|
this.setupBody();
|
||||||
|
}
|
||||||
|
if (this.settings.config.showTime) {
|
||||||
|
this.setupFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this.addHandler("visible", function (state, prevState) {
|
||||||
|
if (state.visible && !prevState.visible){
|
||||||
|
document.body.appendChild(this.el.wrapper);
|
||||||
|
|
||||||
|
var parts = self.elem.value.split(/\s*,\s*/);
|
||||||
|
var startDate = undefined;
|
||||||
|
var startTime = 0;
|
||||||
|
if (self.settings.config.showDate) {
|
||||||
|
startDate = parseDate(parts[0], self.settings);
|
||||||
|
}
|
||||||
|
if (self.settings.config.showTime) {
|
||||||
|
startTime = parseTime(parts[parts.length-1], self.settings);
|
||||||
|
startTime = startTime || 0;
|
||||||
|
}
|
||||||
|
if (!(startDate && startDate.getTime())) {
|
||||||
|
startDate = new Date();
|
||||||
|
startDate = new Date(
|
||||||
|
startDate.getFullYear(),
|
||||||
|
startDate.getMonth(),
|
||||||
|
startDate.getDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var value = new Date(startDate.getTime() + startTime);
|
||||||
|
self.value = value;
|
||||||
|
self.year = value.getFullYear();
|
||||||
|
self.month = value.getMonth();
|
||||||
|
self.day = value.getDate();
|
||||||
|
self.hours = value.getHours();
|
||||||
|
self.minutes = value.getMinutes();
|
||||||
|
self.seconds = value.getSeconds();
|
||||||
|
|
||||||
|
if (self.settings.config.showDate) {
|
||||||
|
self.setHeaderContent();
|
||||||
|
self.setBodyContent();
|
||||||
|
}
|
||||||
|
if (self.settings.config.showTime) {
|
||||||
|
self.setFooterContent();
|
||||||
|
}
|
||||||
|
} else if (!state.visible && prevState.visible) {
|
||||||
|
document.body.removeChild(this.el.wrapper);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DTBox.prototype.setupWrapper = function () {
|
||||||
|
if (!this.el.wrapper) {
|
||||||
|
var el = document.createElement("div");
|
||||||
|
el.classList.add("date-selector-wrapper");
|
||||||
|
Object.defineProperty(this.el, "wrapper", { value: el });
|
||||||
|
}
|
||||||
|
var self = this;
|
||||||
|
var htmlRoot = document.getElementsByTagName('html')[0];
|
||||||
|
function setPosition(e){
|
||||||
|
var minTopSpace = 300;
|
||||||
|
var box = getOffset(self.elem);
|
||||||
|
var config = self.settings.config;
|
||||||
|
var paddingY = config.paddingY || 5;
|
||||||
|
var paddingX = config.paddingX || 5;
|
||||||
|
var top = box.top + self.elem.offsetHeight + paddingY;
|
||||||
|
var left = box.left + paddingX;
|
||||||
|
var bottom = htmlRoot.clientHeight - box.top + paddingY;
|
||||||
|
|
||||||
|
self.el.wrapper.style.left = `${left}px`;
|
||||||
|
if (box.top > minTopSpace && config.direction != 'BOTTOM') {
|
||||||
|
self.el.wrapper.style.bottom = `${bottom}px`;
|
||||||
|
self.el.wrapper.style.top = '';
|
||||||
|
} else {
|
||||||
|
self.el.wrapper.style.top = `${top}px`;
|
||||||
|
self.el.wrapper.style.bottom = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(e) {
|
||||||
|
self.cancelBlur += 1;
|
||||||
|
setTimeout(function(){
|
||||||
|
self.elem.focus();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
setPosition();
|
||||||
|
this.setPosition = setPosition;
|
||||||
|
this.el.wrapper.addEventListener("mousedown", handler, false);
|
||||||
|
this.el.wrapper.addEventListener("touchstart", handler, false);
|
||||||
|
window.addEventListener('resize', this.setPosition);
|
||||||
|
}
|
||||||
|
DTBox.prototype.setupHeader = function () {
|
||||||
|
if (!this.el.header) {
|
||||||
|
var row = document.createElement("div");
|
||||||
|
var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"];
|
||||||
|
row.classList.add("cal-header");
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
var cell = document.createElement("div");
|
||||||
|
cell.classList.add("cal-nav", classes[i]);
|
||||||
|
cell.onclick = this.onHeaderChange.bind(this);
|
||||||
|
row.appendChild(cell);
|
||||||
|
}
|
||||||
|
row.children[0].innerHTML = "<";
|
||||||
|
row.children[2].innerHTML = ">";
|
||||||
|
Object.defineProperty(this.el, "header", { value: row });
|
||||||
|
tryAppendChild(row, this.el.wrapper);
|
||||||
|
}
|
||||||
|
this.setHeaderContent();
|
||||||
|
}
|
||||||
|
DTBox.prototype.setHeaderContent = function () {
|
||||||
|
var content = this.year;
|
||||||
|
if ("DAYS" == this.bodyType) {
|
||||||
|
content = this.month_long + " " + content;
|
||||||
|
} else if ("YEARS" == this.bodyType) {
|
||||||
|
var start = this.year + 10 - (this.year % 10);
|
||||||
|
content = start - 10 + "-" + (start - 1);
|
||||||
|
}
|
||||||
|
this.el.header.children[1].innerText = content;
|
||||||
|
}
|
||||||
|
DTBox.prototype.setupBody = function () {
|
||||||
|
if (!this.el.body) {
|
||||||
|
var el = document.createElement("div");
|
||||||
|
el.classList.add("cal-body");
|
||||||
|
Object.defineProperty(this.el, "body", { value: el });
|
||||||
|
tryAppendChild(el, this.el.wrapper);
|
||||||
|
}
|
||||||
|
var toAppend = null;
|
||||||
|
function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
|
||||||
|
var grid = document.createElement("div");
|
||||||
|
grid.classList.add(className);
|
||||||
|
for (var i = 1; i < rows + 1; i++) {
|
||||||
|
var row = document.createElement("div");
|
||||||
|
row.classList.add("cal-row", "cal-row-" + i);
|
||||||
|
if (i == 1 && firstRowClass) {
|
||||||
|
row.classList.add(firstRowClass);
|
||||||
|
}
|
||||||
|
for (var j = 1; j < cols + 1; j++) {
|
||||||
|
var col = document.createElement("div");
|
||||||
|
col.classList.add("cal-cell", "cal-col-" + j);
|
||||||
|
col.onclick = clickHandler;
|
||||||
|
row.appendChild(col);
|
||||||
|
}
|
||||||
|
grid.appendChild(row);
|
||||||
|
}
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
if ("DAYS" == this.bodyType) {
|
||||||
|
toAppend = this.el.body.calDays;
|
||||||
|
if (!toAppend) {
|
||||||
|
toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
|
||||||
|
for (var i = 0; i < 7; i++) {
|
||||||
|
var cell = toAppend.children[0].children[i];
|
||||||
|
cell.innerText = WEEKDAYS[i].slice(0, 2);
|
||||||
|
cell.onclick = null;
|
||||||
|
}
|
||||||
|
this.el.body.calDays = toAppend;
|
||||||
|
}
|
||||||
|
} else if ("MONTHS" == this.bodyType) {
|
||||||
|
toAppend = this.el.body.calMonths;
|
||||||
|
if (!toAppend) {
|
||||||
|
toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
for (var j = 0; j < 4; j++) {
|
||||||
|
var monthShort = MONTHS[4 * i + j].slice(0, 3);
|
||||||
|
toAppend.children[i].children[j].innerText = monthShort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.el.body.calMonths = toAppend;
|
||||||
|
}
|
||||||
|
} else if ("YEARS" == this.bodyType) {
|
||||||
|
toAppend = this.el.body.calYears;
|
||||||
|
if (!toAppend) {
|
||||||
|
toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
|
||||||
|
this.el.body.calYears = toAppend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
empty(this.el.body);
|
||||||
|
tryAppendChild(toAppend, this.el.body);
|
||||||
|
this.setBodyContent();
|
||||||
|
}
|
||||||
|
DTBox.prototype.setBodyContent = function () {
|
||||||
|
var grid = this.el.body.children[0];
|
||||||
|
var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
|
||||||
|
if ("DAYS" == this.bodyType) {
|
||||||
|
var oneDayMilliSecs = 24 * 60 * 60 * 1000;
|
||||||
|
var start = new Date(this.year, this.month, 1);
|
||||||
|
var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
|
||||||
|
|
||||||
|
grid.children[6].style.display = "";
|
||||||
|
for (var i = 1; i < 7; i++) {
|
||||||
|
for (var j = 0; j < 7; j++) {
|
||||||
|
var cell = grid.children[i].children[j];
|
||||||
|
var month = adjusted.getMonth();
|
||||||
|
var date = adjusted.getDate();
|
||||||
|
|
||||||
|
cell.innerText = date;
|
||||||
|
cell.classList.remove(classes[0], classes[1], classes[2]);
|
||||||
|
if (month != this.month) {
|
||||||
|
if (i == 6 && j == 0) {
|
||||||
|
grid.children[6].style.display = "none";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cell.classList.add(month < this.month ? classes[0] : classes[1]);
|
||||||
|
} else if (isEqualDate(adjusted, this.value)){
|
||||||
|
cell.classList.add(classes[2]);
|
||||||
|
}
|
||||||
|
adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("YEARS" == this.bodyType) {
|
||||||
|
var year = this.year - (this.year % 10) - 1;
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
for (j = 0; j < 4; j++) {
|
||||||
|
grid.children[i].children[j].innerText = year;
|
||||||
|
year += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grid.children[0].children[0].classList.add(classes[0]);
|
||||||
|
grid.children[2].children[3].classList.add(classes[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} e */
|
||||||
|
DTBox.prototype.onTimeChange = function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.type == 'mousedown') {
|
||||||
|
this.cancelBlur += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var el = e.target;
|
||||||
|
this[el.name] = parseInt(el.value) || 0;
|
||||||
|
this.setupFooter();
|
||||||
|
if (e.type == 'change') {
|
||||||
|
var self = this;
|
||||||
|
setTimeout(function(){
|
||||||
|
self.elem.focus();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
this.setInputValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
DTBox.prototype.setupFooter = function() {
|
||||||
|
if (!this.el.footer) {
|
||||||
|
var footer = document.createElement("div");
|
||||||
|
var handler = this.onTimeChange.bind(this);
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function makeRow(label, name, range, changeHandler) {
|
||||||
|
var row = document.createElement("div");
|
||||||
|
row.classList.add('cal-time');
|
||||||
|
|
||||||
|
var labelCol = row.appendChild(document.createElement("div"));
|
||||||
|
labelCol.classList.add('cal-time-label');
|
||||||
|
labelCol.innerText = label;
|
||||||
|
|
||||||
|
var valueCol = row.appendChild(document.createElement("div"));
|
||||||
|
valueCol.classList.add('cal-time-value');
|
||||||
|
valueCol.innerText = '00';
|
||||||
|
|
||||||
|
var inputCol = row.appendChild(document.createElement("div"));
|
||||||
|
var slider = inputCol.appendChild(document.createElement("input"));
|
||||||
|
Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
|
||||||
|
Object.defineProperty(footer, name, {value: slider});
|
||||||
|
inputCol.classList.add('cal-time-slider');
|
||||||
|
slider.onchange = changeHandler;
|
||||||
|
slider.oninput = changeHandler;
|
||||||
|
slider.onmousedown = changeHandler;
|
||||||
|
self[name] = self[name] || parseInt(slider.value) || 0;
|
||||||
|
footer.appendChild(row)
|
||||||
|
}
|
||||||
|
makeRow('HH:', 'hours', 23, handler);
|
||||||
|
makeRow('MM:', 'minutes', 59, handler);
|
||||||
|
makeRow('SS:', 'seconds', 59, handler);
|
||||||
|
|
||||||
|
footer.classList.add("cal-footer");
|
||||||
|
Object.defineProperty(this.el, "footer", { value: footer });
|
||||||
|
tryAppendChild(footer, this.el.wrapper);
|
||||||
|
}
|
||||||
|
this.setFooterContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
DTBox.prototype.setFooterContent = function() {
|
||||||
|
if (this.el.footer) {
|
||||||
|
var footer = this.el.footer;
|
||||||
|
footer.hours.value = this.hours;
|
||||||
|
footer.children[0].children[1].innerText = padded(this.hours, 2);
|
||||||
|
footer.minutes.value = this.minutes;
|
||||||
|
footer.children[1].children[1].innerText = padded(this.minutes, 2);
|
||||||
|
footer.seconds.value = this.seconds;
|
||||||
|
footer.children[2].children[1].innerText = padded(this.seconds, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DTBox.prototype.setInputValue = function() {
|
||||||
|
var date = new Date(this.year, this.month, this.day);
|
||||||
|
var strings = [];
|
||||||
|
if (this.settings.config.showDate) {
|
||||||
|
strings.push(renderDate(date, this.settings));
|
||||||
|
}
|
||||||
|
if (this.settings.config.showTime) {
|
||||||
|
var joined = new Date(date.getTime() + this.time);
|
||||||
|
strings.push(renderTime(joined, this.settings));
|
||||||
|
}
|
||||||
|
this.elem.value = strings.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
DTBox.prototype.onDateSelected = function (e) {
|
||||||
|
var row = e.target.parentNode;
|
||||||
|
var date = parseInt(e.target.innerText);
|
||||||
|
if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
|
||||||
|
this.month += 1;
|
||||||
|
} else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
|
||||||
|
this.month -= 1;
|
||||||
|
}
|
||||||
|
this.day = parseInt(e.target.innerText);
|
||||||
|
this.value = new Date(this.year, this.month, this.day);
|
||||||
|
this.setInputValue();
|
||||||
|
this.setHeaderContent();
|
||||||
|
this.setBodyContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} e */
|
||||||
|
DTBox.prototype.onMonthSelected = function (e) {
|
||||||
|
var col = 0;
|
||||||
|
var row = 2;
|
||||||
|
var cell = e.target;
|
||||||
|
if (cell.parentNode.nextSibling){
|
||||||
|
row = cell.parentNode.previousSibling ? 1: 0;
|
||||||
|
}
|
||||||
|
if (cell.previousSibling) {
|
||||||
|
col = 3;
|
||||||
|
if (cell.nextSibling) {
|
||||||
|
col = cell.previousSibling.previousSibling ? 2 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.month = 4 * row + col;
|
||||||
|
this.bodyType = "DAYS";
|
||||||
|
this.setHeaderContent();
|
||||||
|
this.setupBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} e */
|
||||||
|
DTBox.prototype.onYearSelected = function (e) {
|
||||||
|
this.year = parseInt(e.target.innerText);
|
||||||
|
this.bodyType = "MONTHS";
|
||||||
|
this.setHeaderContent();
|
||||||
|
this.setupBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} e */
|
||||||
|
DTBox.prototype.onHeaderChange = function (e) {
|
||||||
|
var cell = e.target;
|
||||||
|
if (cell.previousSibling && cell.nextSibling) {
|
||||||
|
var idx = BODYTYPES.indexOf(this.bodyType);
|
||||||
|
if (idx < 0 || !BODYTYPES[idx + 1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.bodyType = BODYTYPES[idx + 1];
|
||||||
|
this.setupBody();
|
||||||
|
} else {
|
||||||
|
var sign = cell.previousSibling ? 1 : -1;
|
||||||
|
switch (this.bodyType) {
|
||||||
|
case "DAYS":
|
||||||
|
this.month += sign * 1;
|
||||||
|
break;
|
||||||
|
case "MONTHS":
|
||||||
|
this.year += sign * 1;
|
||||||
|
break;
|
||||||
|
case "YEARS":
|
||||||
|
this.year += sign * 10;
|
||||||
|
}
|
||||||
|
if (this.month > 11 || this.month < 0) {
|
||||||
|
this.year += Math.floor(this.month / 11);
|
||||||
|
this.month = this.month > 11 ? 0 : 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setHeaderContent();
|
||||||
|
this.setBodyContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} elem
|
||||||
|
* @returns {{left:number, top:number}}
|
||||||
|
*/
|
||||||
|
function getOffset(elem) {
|
||||||
|
var box = elem.getBoundingClientRect();
|
||||||
|
var left = window.pageXOffset !== undefined ? window.pageXOffset :
|
||||||
|
(document.documentElement || document.body.parentNode || document.body).scrollLeft;
|
||||||
|
var top = window.pageYOffset !== undefined ? window.pageYOffset :
|
||||||
|
(document.documentElement || document.body.parentNode || document.body).scrollTop;
|
||||||
|
return { left: box.left + left, top: box.top + top };
|
||||||
|
}
|
||||||
|
function empty(e) {
|
||||||
|
for (; e.children.length; ) e.removeChild(e.children[0]);
|
||||||
|
}
|
||||||
|
function tryAppendChild(newChild, refNode) {
|
||||||
|
try {
|
||||||
|
refNode.appendChild(newChild);
|
||||||
|
return newChild;
|
||||||
|
} catch (e) {
|
||||||
|
console.trace(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @class */
|
||||||
|
function hookFuncs() {
|
||||||
|
/** @type {Handlers} */
|
||||||
|
this._funcs = {};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @param {Function} func
|
||||||
|
*/
|
||||||
|
hookFuncs.prototype.add = function(key, func){
|
||||||
|
if (!this._funcs[key]){
|
||||||
|
this._funcs[key] = [];
|
||||||
|
}
|
||||||
|
this._funcs[key].push(func)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {String} key
|
||||||
|
* @returns {Function[]} handlers
|
||||||
|
*/
|
||||||
|
hookFuncs.prototype.get = function(key){
|
||||||
|
return this._funcs[key] ? this._funcs[key] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array.<string>} arr
|
||||||
|
* @param {String} string
|
||||||
|
* @returns {Array.<string>} sorted string
|
||||||
|
*/
|
||||||
|
function sortByStringIndex(arr, string) {
|
||||||
|
return arr.sort(function(a, b){
|
||||||
|
var h = string.indexOf(a);
|
||||||
|
var l = string.indexOf(b);
|
||||||
|
var rank = 0;
|
||||||
|
if (h < l) {
|
||||||
|
rank = -1;
|
||||||
|
} else if (l < h) {
|
||||||
|
rank = 1;
|
||||||
|
} else if (a.length > b.length) {
|
||||||
|
rank = -1;
|
||||||
|
} else if (b.length > a.length) {
|
||||||
|
rank = 1;
|
||||||
|
}
|
||||||
|
return rank;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove keys from array that are not in format
|
||||||
|
* @param {string[]} keys
|
||||||
|
* @param {string} format
|
||||||
|
* @returns {string[]} new filtered array
|
||||||
|
*/
|
||||||
|
function filterFormatKeys(keys, format) {
|
||||||
|
var out = [];
|
||||||
|
var formatIdx = 0;
|
||||||
|
for (var i = 0; i<keys.length; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
if (format.slice(formatIdx).indexOf(key) > -1) {
|
||||||
|
formatIdx += key.length;
|
||||||
|
out.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {StringNumObj} FormatObj
|
||||||
|
* @param {string} value
|
||||||
|
* @param {string} format
|
||||||
|
* @param {FormatObj} formatObj
|
||||||
|
* @param {function(Object.<string, hookFuncs>): null} setHooks
|
||||||
|
* @returns {FormatObj} formatObj
|
||||||
|
*/
|
||||||
|
function parseData(value, format, formatObj, setHooks) {
|
||||||
|
var hooks = {
|
||||||
|
canSkip: new hookFuncs(),
|
||||||
|
updateValue: new hookFuncs(),
|
||||||
|
}
|
||||||
|
var keys = sortByStringIndex(Object.keys(formatObj), format);
|
||||||
|
var filterdKeys = filterFormatKeys(keys, format);
|
||||||
|
var vstart = 0; // value start
|
||||||
|
if (setHooks) {
|
||||||
|
setHooks(hooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
var fstart = format.indexOf(key);
|
||||||
|
var _vstart = vstart; // next value start
|
||||||
|
var val = null;
|
||||||
|
var canSkip = false;
|
||||||
|
var funcs = hooks.canSkip.get(key);
|
||||||
|
|
||||||
|
vstart = vstart || fstart;
|
||||||
|
|
||||||
|
for (var j = 0; j < funcs.length; j++) {
|
||||||
|
if (funcs[j](formatObj)){
|
||||||
|
canSkip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fstart > -1 && !canSkip) {
|
||||||
|
var sep = null;
|
||||||
|
var stop = vstart + key.length;
|
||||||
|
var fnext = -1;
|
||||||
|
var nextKeyIdx = i + 1;
|
||||||
|
_vstart += key.length; // set next value start if current key is found
|
||||||
|
|
||||||
|
// get next format token used to determine separator
|
||||||
|
while (fnext == -1 && nextKeyIdx < keys.length){
|
||||||
|
var nextKey = keys[nextKeyIdx];
|
||||||
|
nextKeyIdx += 1;
|
||||||
|
if (filterdKeys.indexOf(nextKey) === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
|
||||||
|
}
|
||||||
|
if (fnext > -1){
|
||||||
|
sep = format.slice(stop, fnext);
|
||||||
|
if (sep) {
|
||||||
|
var _stop = value.slice(vstart).indexOf(sep);
|
||||||
|
if (_stop && _stop > -1){
|
||||||
|
stop = _stop + vstart;
|
||||||
|
_vstart = stop + sep.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val = parseInt(value.slice(vstart, stop));
|
||||||
|
|
||||||
|
var funcs = hooks.updateValue.get(key);
|
||||||
|
for (var k = 0; k < funcs.length; k++) {
|
||||||
|
val = funcs[k](val, formatObj, vstart, stop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formatObj[key] = { index: vstart, value: val };
|
||||||
|
vstart = _vstart; // set next value start
|
||||||
|
}
|
||||||
|
return formatObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} value
|
||||||
|
* @param {DTS} settings
|
||||||
|
* @returns {Date} date object
|
||||||
|
*/
|
||||||
|
function parseDate(value, settings) {
|
||||||
|
/** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
|
||||||
|
var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
|
||||||
|
var format = ((settings.dateFormat) || '').toLowerCase();
|
||||||
|
if (!format) {
|
||||||
|
throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
|
||||||
|
}
|
||||||
|
var formatObj = parseData(value, format, formatObj, function(hooks){
|
||||||
|
hooks.canSkip.add("yy", function(data){
|
||||||
|
return data["yyyy"].value;
|
||||||
|
});
|
||||||
|
hooks.updateValue.add("yy", function(val){
|
||||||
|
return 100 * Math.floor(new Date().getFullYear() / 100) + val;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var year = formatObj["yyyy"].value || formatObj["yy"].value;
|
||||||
|
var month = formatObj["mm"].value - 1;
|
||||||
|
var date = formatObj["dd"].value;
|
||||||
|
var result = new Date(year, month, date);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} value
|
||||||
|
* @param {DTS} settings
|
||||||
|
* @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
|
||||||
|
*/
|
||||||
|
function parseTime(value, settings) {
|
||||||
|
var format = ((settings.timeFormat) || '').toLowerCase();
|
||||||
|
if (!format) {
|
||||||
|
throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
|
||||||
|
var formatObj = {hh:null, mm:null, ss:null, a:null};
|
||||||
|
var formatObj = parseData(value, format, formatObj, function(hooks){
|
||||||
|
hooks.updateValue.add("a", function(val, data, start, stop){
|
||||||
|
return value.slice(start, start + 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var hours = formatObj["hh"].value;
|
||||||
|
var minutes = formatObj["mm"].value;
|
||||||
|
var seconds = formatObj["ss"].value;
|
||||||
|
var am_pm = formatObj["a"].value;
|
||||||
|
var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
|
||||||
|
if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
|
||||||
|
if (am_pm_lower == 'am' && hours == 12){
|
||||||
|
hours = 0;
|
||||||
|
} else if (am_pm_lower == 'pm') {
|
||||||
|
hours += 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Date} value
|
||||||
|
* @param {DTS} settings
|
||||||
|
* @returns {String} date string
|
||||||
|
*/
|
||||||
|
function renderDate(value, settings) {
|
||||||
|
var format = settings.dateFormat.toLowerCase();
|
||||||
|
var date = value.getDate();
|
||||||
|
var month = value.getMonth() + 1;
|
||||||
|
var year = value.getFullYear();
|
||||||
|
var yearShort = year % 100;
|
||||||
|
var formatObj = {
|
||||||
|
dd: date < 10 ? "0" + date : date,
|
||||||
|
mm: month < 10 ? "0" + month : month,
|
||||||
|
yyyy: year,
|
||||||
|
yy: yearShort < 10 ? "0" + yearShort : yearShort
|
||||||
|
};
|
||||||
|
var str = format.replace(settings.dateFormatRegEx, function (found) {
|
||||||
|
return formatObj[found];
|
||||||
|
});
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Date} value
|
||||||
|
* @param {DTS} settings
|
||||||
|
* @returns {String} date string
|
||||||
|
*/
|
||||||
|
function renderTime(value, settings) {
|
||||||
|
var Format = settings.timeFormat;
|
||||||
|
var format = Format.toLowerCase();
|
||||||
|
var hours = value.getHours();
|
||||||
|
var minutes = value.getMinutes();
|
||||||
|
var seconds = value.getSeconds();
|
||||||
|
var am_pm = null;
|
||||||
|
var hh_am_pm = null;
|
||||||
|
if (format.indexOf('a') > -1) {
|
||||||
|
am_pm = hours >= 12 ? 'pm' : 'am';
|
||||||
|
am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
|
||||||
|
hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
|
||||||
|
}
|
||||||
|
var formatObj = {
|
||||||
|
hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
|
||||||
|
mm: minutes < 10 ? "0" + minutes : minutes,
|
||||||
|
ss: seconds < 10 ? "0" + seconds : seconds,
|
||||||
|
a: am_pm,
|
||||||
|
};
|
||||||
|
var str = format.replace(settings.timeFormatRegEx, function (found) {
|
||||||
|
return formatObj[found];
|
||||||
|
});
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if two dates are equal
|
||||||
|
* @param {Date} date1
|
||||||
|
* @param {Date} date2
|
||||||
|
* @returns {Boolean} true or false
|
||||||
|
*/
|
||||||
|
function isEqualDate(date1, date2) {
|
||||||
|
if (!(date1 && date2)) return false;
|
||||||
|
return (date1.getFullYear() == date2.getFullYear() &&
|
||||||
|
date1.getMonth() == date2.getMonth() &&
|
||||||
|
date1.getDate() == date2.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Number} val
|
||||||
|
* @param {Number} pad
|
||||||
|
* @param {*} default_val
|
||||||
|
* @returns {String} padded string
|
||||||
|
*/
|
||||||
|
function padded(val, pad, default_val) {
|
||||||
|
var default_val = default_val || 0;
|
||||||
|
var valStr = '' + (parseInt(val) || default_val);
|
||||||
|
var diff = Math.max(pad, valStr.length) - valStr.length;
|
||||||
|
return ('' + default_val).repeat(diff) + valStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template X
|
||||||
|
* @template Y
|
||||||
|
* @param {X} obj
|
||||||
|
* @param {Y} objDefaults
|
||||||
|
* @returns {X|Y} merged object
|
||||||
|
*/
|
||||||
|
function setDefaults(obj, objDefaults) {
|
||||||
|
var keys = Object.keys(objDefaults);
|
||||||
|
for (var i=0; i<keys.length; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
obj[key] = objDefaults[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.dtsel = Object.create({},{
|
||||||
|
DTS: { value: DTS },
|
||||||
|
DTObj: { value: DTBox },
|
||||||
|
fn: {
|
||||||
|
value: Object.defineProperties({}, {
|
||||||
|
empty: { value: empty },
|
||||||
|
appendAfter: {
|
||||||
|
value: function (newElem, refNode) {
|
||||||
|
refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getOffset: { value: getOffset },
|
||||||
|
parseDate: { value: parseDate },
|
||||||
|
renderDate: { value: renderDate },
|
||||||
|
parseTime: {value: parseTime},
|
||||||
|
renderTime: {value: renderTime},
|
||||||
|
setDefaults: {value: setDefaults},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
7
web/static/js/iro.js
Normal file
2
web/static/js/js.cookie.min.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/*! js-cookie v3.0.0-rc.0 | MIT */
|
||||||
|
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var r=e.Cookies,n=e.Cookies=t();n.noConflict=function(){return e.Cookies=r,n}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)e[n]=r[n]}return e}var t={read:function(e){return e.replace(/%3B/g,";")},write:function(e){return e.replace(/;/g,"%3B")}};return function r(n,i){function o(r,o,u){if("undefined"!=typeof document){"number"==typeof(u=e({},i,u)).expires&&(u.expires=new Date(Date.now()+864e5*u.expires)),u.expires&&(u.expires=u.expires.toUTCString()),r=t.write(r).replace(/=/g,"%3D"),o=n.write(String(o),r);var c="";for(var f in u)u[f]&&(c+="; "+f,!0!==u[f]&&(c+="="+u[f].split(";")[0]));return document.cookie=r+"="+o+c}}return Object.create({set:o,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var r=document.cookie?document.cookie.split("; "):[],i={},o=0;o<r.length;o++){var u=r[o].split("="),c=u.slice(1).join("="),f=t.read(u[0]).replace(/%3D/g,"=");if(i[f]=n.read(c,f),e===f)break}return e?i[e]:i}},remove:function(t,r){o(t,"",e({},r,{expires:-1}))},withAttributes:function(t){return r(this.converter,e({},this.attributes,t))},withConverter:function(t){return r(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(i)},converter:{value:Object.freeze(n)}})}(t,{path:"/"})});
|
1
web/static/js/luxon.min.js
vendored
Normal file
BIN
web/static/webfonts/fa-brands-400.eot
Normal file
3633
web/static/webfonts/fa-brands-400.svg
Normal file
After Width: | Height: | Size: 712 KiB |
BIN
web/static/webfonts/fa-brands-400.ttf
Normal file
BIN
web/static/webfonts/fa-brands-400.woff
Normal file
BIN
web/static/webfonts/fa-brands-400.woff2
Normal file
BIN
web/static/webfonts/fa-duotone-900.eot
Normal file
15055
web/static/webfonts/fa-duotone-900.svg
Normal file
After Width: | Height: | Size: 2.5 MiB |
BIN
web/static/webfonts/fa-duotone-900.ttf
Normal file
BIN
web/static/webfonts/fa-duotone-900.woff
Normal file
BIN
web/static/webfonts/fa-duotone-900.woff2
Normal file
BIN
web/static/webfonts/fa-light-300.eot
Normal file
12330
web/static/webfonts/fa-light-300.svg
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
web/static/webfonts/fa-light-300.ttf
Normal file
BIN
web/static/webfonts/fa-light-300.woff
Normal file
BIN
web/static/webfonts/fa-light-300.woff2
Normal file
BIN
web/static/webfonts/fa-regular-400.eot
Normal file
11256
web/static/webfonts/fa-regular-400.svg
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
web/static/webfonts/fa-regular-400.ttf
Normal file
BIN
web/static/webfonts/fa-regular-400.woff
Normal file
BIN
web/static/webfonts/fa-regular-400.woff2
Normal file
BIN
web/static/webfonts/fa-solid-900.eot
Normal file
9588
web/static/webfonts/fa-solid-900.svg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
web/static/webfonts/fa-solid-900.ttf
Normal file
BIN
web/static/webfonts/fa-solid-900.woff
Normal file
BIN
web/static/webfonts/fa-solid-900.woff2
Normal file
192
web/templates/base.html.tera
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{% block init %}
|
||||||
|
{% endblock %}
|
||||||
|
<html lang="EN">
|
||||||
|
<head>
|
||||||
|
<meta name="description" content="The most powerful Discord Reminders Bot">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="yandex-verification" content="bb77b8681eb64a90" />
|
||||||
|
<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho" />
|
||||||
|
|
||||||
|
<!-- favicon -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/static/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
<title>Reminder Bot | {{ title }}</title>
|
||||||
|
|
||||||
|
<!-- styles -->
|
||||||
|
<link rel="stylesheet" href="/static/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/fa.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/font.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% if flashed_message %}
|
||||||
|
<div class="notification is-{{ flashed_grade }} flash-message is-active">
|
||||||
|
<span class="icon"><i class="far fa-exclamation-circle"></i></span> {{ flashed_message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<nav class="navbar is-dark is-spaced is-size-4" role="navigation" aria-label="main navigation">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item" href="/">
|
||||||
|
<figure class="image">
|
||||||
|
<img src="/static/img/logo_flat.jpg" alt="Reminder Bot Logo" class="is-rounded" style="width: auto;">
|
||||||
|
</figure>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a role="button" class="navbar-burger is-right" aria-label="menu" aria-expanded="false" data-target="pageNavbar">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-menu">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="navbar-item">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item">
|
||||||
|
<i class="fab fa-github"></i>
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item">
|
||||||
|
<i class="fab fa-discord"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-end">
|
||||||
|
<div class="navbar-item">
|
||||||
|
<a class="button is-rounded is-light" href="/dashboard">
|
||||||
|
<p>
|
||||||
|
Dashboard <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-menu is-hidden-desktop" id="pageNavbar">
|
||||||
|
<a class="navbar-item">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item">
|
||||||
|
<i class="fab fa-github"></i>
|
||||||
|
</a>
|
||||||
|
<a class="navbar-item">
|
||||||
|
<i class="fab fa-discord"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="navbar-item">
|
||||||
|
<a href="/dashboard">
|
||||||
|
<p>
|
||||||
|
Dashboard <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% if not hide_title_block %}
|
||||||
|
<section class="hero is-small is-dark">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="">
|
||||||
|
<h1 class="title is-1">{{ page_title }}</h1>
|
||||||
|
<h2 class="subtitle is-3">{{ page_subtitle }}
|
||||||
|
{% if page_emoji %}
|
||||||
|
<span class="icon"><i class="fas {{ page_emoji }}"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if show_invite %}
|
||||||
|
<div class="hero-foot has-text-centered">
|
||||||
|
<a class="button is-size-4 is-rounded is-success" href="https://invite.reminder-bot.com">
|
||||||
|
<p class="is-size-4">
|
||||||
|
Add to your Server <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% elif show_contact %}
|
||||||
|
<div class="hero-foot has-text-centered">
|
||||||
|
<a class="button is-size-4 is-rounded is-primary" href="https://discord.jellywx.com">
|
||||||
|
<p class="is-size-4">
|
||||||
|
Join Discord <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% elif show_login %}
|
||||||
|
<div class="hero-foot has-text-centered">
|
||||||
|
<a class="button is-size-4 is-rounded is-light" href="/oauth/login">
|
||||||
|
<p class="is-size-4">
|
||||||
|
Login with Discord <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 160">
|
||||||
|
<path transform="scale(1, 0.5)" fill="#98dc9a" fill-opacity="1" d="M0,288L80,266.7C160,245,320,203,480,202.7C640,203,800,245,960,218.7C1120,192,1280,96,1360,48L1440,0L1440,0L1360,0C1280,0,1120,0,960,0C800,0,640,0,480,0C320,0,160,0,80,0L0,0Z"></path>
|
||||||
|
<path transform="scale(1, 0.5)" fill="#363636" fill-opacity="1" d="M0,224L60,202.7C120,181,240,139,360,138.7C480,139,600,181,720,197.3C840,213,960,203,1080,176C1200,149,1320,107,1380,85.3L1440,64L1440,0L1380,0C1320,0,1200,0,1080,0C960,0,840,0,720,0C600,0,480,0,360,0C240,0,120,0,60,0L0,0Z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="content has-text-centered">
|
||||||
|
<p>
|
||||||
|
<strong>Reminder Bot</strong>, created by <a href="https://github.com/JellyWX"><strong>JellyWX</strong></a>
|
||||||
|
<br>
|
||||||
|
<a href="/cookies">Cookies</a> | <a href="/privacy">Privacy Policy</a> | <a href="/terms">Terms of Service</a>
|
||||||
|
<br>
|
||||||
|
<a href="https://patreon.com/jellywx"><strong>Patreon</strong></a> | <a href="https://discord.jellywx.com"><strong>Discord</strong></a> | <a href="https://github.com/JellyWX"><strong>GitHub</strong></a>
|
||||||
|
<br>
|
||||||
|
or, <a href="mailto:jude@jellywx.com">Email me</a>
|
||||||
|
<br>
|
||||||
|
<span class="icon"><i class="fab fa-monero"></i></span>
|
||||||
|
<strong>XMR</strong> 49oP6DzzEzdC6VkxE6hGoLSRw5awvEx5cGGXH327tck15LWk8SFgLUAjS2yZqssMWN3JPRraR68ApHi4GGSLtCDmLbF7euH
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
// Get all "navbar-burger" elements
|
||||||
|
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||||
|
|
||||||
|
// Check if there are any navbar burgers
|
||||||
|
if ($navbarBurgers.length > 0) {
|
||||||
|
|
||||||
|
// Add a click event on each of them
|
||||||
|
$navbarBurgers.forEach( el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
|
||||||
|
// Get the target from the "data-target" attribute
|
||||||
|
const target = el.dataset.target;
|
||||||
|
const $target = document.getElementById(target);
|
||||||
|
|
||||||
|
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||||
|
el.classList.toggle('is-active');
|
||||||
|
$target.classList.toggle('is-active');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
61
web/templates/cookies.html.tera
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block init %}
|
||||||
|
{% set title = "Cookies" %}
|
||||||
|
|
||||||
|
{% set page_title = "Cookies" %}
|
||||||
|
{% set page_subtitle = "Data this website stores on your computer" %}
|
||||||
|
{% set page_emoji = "fa-gingerbread-man" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">User Data</h2>
|
||||||
|
<p class="is-size-5 pl-6">
|
||||||
|
This website uses some necessary cookies and session data to operate. None of this can be disabled, since
|
||||||
|
it is all necessary for the site to function. <strong>However,</strong> it is worth mentioning that all of
|
||||||
|
this data is <strong>first-party only</strong>, i.e we use no tracking scripts like Google Analytics, and
|
||||||
|
no adverts are served on this site.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">Cookies</h2>
|
||||||
|
<p class="is-size-5 pl-6">
|
||||||
|
Cookies are data that are persistent between browser restarts. Cookies are read and written by the website
|
||||||
|
running on your computer, not by our server.
|
||||||
|
<br>
|
||||||
|
Cookies store information on your preferences, including if <strong>you prefer AM/PM or 24 hour
|
||||||
|
clock</strong>, if <strong>you have closed the cookie popup before</strong>, and <strong>what
|
||||||
|
order you have the servers in on the dashboard</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">Session Storage</h2>
|
||||||
|
<p class="is-size-5 pl-6">
|
||||||
|
Session data are data that is stored just for the active browser session. Session storage is read and
|
||||||
|
written by our server and cannot be modified on your computer.
|
||||||
|
<br>
|
||||||
|
Session data stores an <strong>access token provided by Discord</strong>, used to retrieve information
|
||||||
|
about your account. Also stored is an <strong>internal ID for use with our API</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">How Can We Trust You?</h2>
|
||||||
|
<p class="is-size-5 pl-6">
|
||||||
|
Feel free to audit this website. Go to our <a href="https://github.com/reminder-bot">GitHub</a> to get started, or just press <kbd>F12</kbd>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
805
web/templates/dashboard.html.tera
Normal file
@ -0,0 +1,805 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="EN">
|
||||||
|
<head>
|
||||||
|
<meta name="description" content="The most powerful Discord Reminders Bot">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="yandex-verification" content="bb77b8681eb64a90"/>
|
||||||
|
<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
|
||||||
|
|
||||||
|
<!-- favicon -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180"
|
||||||
|
href="/static/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32"
|
||||||
|
href="/static/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16"
|
||||||
|
href="/static/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/static/favicon/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
<title>Reminder Bot | Dashboard</title>
|
||||||
|
|
||||||
|
<!-- styles -->
|
||||||
|
<link rel="stylesheet" href="/static/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/fa.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/font.css">
|
||||||
|
<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"
|
||||||
|
aria-label="main navigation">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item" href="/">
|
||||||
|
<figure class="image">
|
||||||
|
<img src="/static/img/logo_flat.jpg" alt="Reminder Bot Logo">
|
||||||
|
</figure>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="navbar-item pageTitle">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a role="button" class="navbar-burger is-right" aria-label="menu" aria-expanded="false"
|
||||||
|
data-target="mobileSidebar">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- dead image used to check which other images are dead -->
|
||||||
|
<img style="display: none;" 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="addImageModal">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<label class="modal-card-title" for="urlInput">Enter Image URL</label>
|
||||||
|
<button class="delete close-modal" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<input class="input" id="urlInput" placeholder="Image URL...">
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<button class="button is-success">Save</button>
|
||||||
|
<button class="button close-modal">Cancel</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close is-large close-modal" aria-label="close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="pickColorModal">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<label class="modal-card-title" for="colorInput">Select Color</label>
|
||||||
|
<button class="delete close-modal" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<div style="display: flex; justify-content: center">
|
||||||
|
<div id="colorpicker"></div>
|
||||||
|
</div>
|
||||||
|
<input class="input" id="colorInput">
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<button class="button is-success">Save</button>
|
||||||
|
<button class="button close-modal">Cancel</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close is-large close-modal" aria-label="close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="chooseTimezoneModal">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<label class="modal-card-title" for="urlInput">Update Timezone</label>
|
||||||
|
<button class="delete close-modal" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<p>
|
||||||
|
Your configured timezone is: <strong><span class="set-timezone">%browsertimezone%</span></strong> (<span class="set-time">HH:mm</span>)
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Your browser timezone is: <strong><span class="browser-timezone">%browsertimezone%</span></strong> (<span class="browser-time">HH:mm</span>)
|
||||||
|
<br>
|
||||||
|
Your bot timezone is: <strong><span class="bot-timezone">%bottimezone</span></strong> (<span class="bot-time">HH:mm</span>)
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<button class="button is-success close-modal" id="set-browser-timezone">Use Browser Timezone</button>
|
||||||
|
<button class="button is-link close-modal" id="set-bot-timezone">Use Bot Timezone</button>
|
||||||
|
<button class="button is-warning close-modal" id="update-bot-timezone">Set Bot Timezone</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close is-large close-modal" aria-label="close"></button>
|
||||||
|
</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;">
|
||||||
|
<a href="/">
|
||||||
|
<div class="brand">
|
||||||
|
<img src="/static/img/logo_flat.jpg" alt="Reminder bot logo"
|
||||||
|
class="dashboard-brand">
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 160">
|
||||||
|
<g transform="scale(1, 0.5)">
|
||||||
|
<path fill="#8fb677" fill-opacity="1"
|
||||||
|
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;">
|
||||||
|
<p class="menu-label">
|
||||||
|
Personal
|
||||||
|
</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
<a class="switch-pane" data-pane="personal">
|
||||||
|
<span class="icon"><i class="fas fa-map-pin"></i></span> @%username%
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="menu-label">
|
||||||
|
Servers
|
||||||
|
</p>
|
||||||
|
<ul class="menu-list guildList">
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<div class="aside-footer" style="margin-top: auto;">
|
||||||
|
<p class="menu-label">
|
||||||
|
Settings
|
||||||
|
</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
<a class="show-modal" data-modal="chooseTimezoneModal">
|
||||||
|
<span class="icon"><i class="fas fa-map-marked"></i></span> Timezone
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-sidebar mobile-sidebar is-hidden-desktop" id="mobileSidebar">
|
||||||
|
<a href="/">
|
||||||
|
<div class="brand">
|
||||||
|
<img src="/static/img/logo_flat.jpg" alt="Reminder bot logo"
|
||||||
|
class="dashboard-brand">
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 160">
|
||||||
|
<g transform="scale(1, 0.5)">
|
||||||
|
<path fill="#8fb677" fill-opacity="1"
|
||||||
|
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;">
|
||||||
|
<p class="menu-label">
|
||||||
|
Personal
|
||||||
|
</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
<a class="switch-pane" data-pane="personal">
|
||||||
|
<span class="icon"><i class="fas fa-map-pin"></i></span> @%username%
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="menu-label">
|
||||||
|
Servers
|
||||||
|
</p>
|
||||||
|
<ul class="menu-list guildList">
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<div class="aside-footer" style="margin-top: auto;">
|
||||||
|
<p class="menu-label">
|
||||||
|
Settings
|
||||||
|
</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
<a class="show-modal" data-modal="chooseTimezoneModal">
|
||||||
|
<span class="icon"><i class="fas fa-map-pin"></i></span> Timezone
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- main content -->
|
||||||
|
<div class="column is-main-content">
|
||||||
|
<p class="title pageTitle" style="margin-left: 12px;"></p>
|
||||||
|
<section id="welcome">
|
||||||
|
<div class="has-text-centered" style="height: 100%; padding-top: 30vh;">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<!-- /main content -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="content has-text-centered">
|
||||||
|
<p>
|
||||||
|
<strong>Reminder Bot</strong>, created by <a href="https://github.com/JellyWX"><strong>JellyWX</strong></a>
|
||||||
|
<br>
|
||||||
|
<a href="https://patreon.com/jellywx"><strong>Patreon</strong></a> | <a
|
||||||
|
href="https://discord.jellywx.com"><strong>Discord</strong></a> | <a
|
||||||
|
href="https://github.com/JellyWX"><strong>GitHub</strong></a>
|
||||||
|
<br>
|
||||||
|
or, <a href="mailto:jude@jellywx.com">Email me</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<template id="embedFieldTemplate">
|
||||||
|
<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"
|
||||||
|
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"
|
||||||
|
placeholder="Field Value..."
|
||||||
|
maxlength="1024" id="embedFieldValue" name="embed_field_value[]"
|
||||||
|
rows="1"></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="guildListEntry">
|
||||||
|
<li>
|
||||||
|
<a class="switch-pane" data-pane="guild">
|
||||||
|
<span class="icon"><i class="fas fa-map-pin"></i></span> %guildname%
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="guildReminder">
|
||||||
|
{% include "reminder_dashboard/guild_reminder" %}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="personalReminder">
|
||||||
|
{% include "reminder_dashboard/personal_reminder" %}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function resize_textareas() {
|
||||||
|
document.querySelectorAll('textarea.autoresize').forEach((element) => {
|
||||||
|
element.style.height = "";
|
||||||
|
element.style.height = element.scrollHeight + 3 + "px";
|
||||||
|
|
||||||
|
element.addEventListener('input', () => {
|
||||||
|
element.style.height = "";
|
||||||
|
element.style.height = element.scrollHeight + 3 + "px";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function switch_pane(selector) {
|
||||||
|
document.querySelectorAll('aside a').forEach((el) => {
|
||||||
|
el.classList.remove('is-active');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('div.is-main-content > section').forEach((el) => {
|
||||||
|
el.classList.add('is-hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(selector).classList.remove('is-hidden');
|
||||||
|
|
||||||
|
resize_textareas();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_select(sel) {
|
||||||
|
if (sel.selectedOptions[0].dataset['webhookAvatar']) {
|
||||||
|
sel
|
||||||
|
.closest('div.reminderContent')
|
||||||
|
.querySelector('img.discord-avatar')
|
||||||
|
.src = sel.selectedOptions[0].dataset['webhookAvatar'];
|
||||||
|
} else {
|
||||||
|
sel
|
||||||
|
.closest('div.reminderContent')
|
||||||
|
.querySelector('img.discord-avatar')
|
||||||
|
.src = '';
|
||||||
|
}
|
||||||
|
if (sel.selectedOptions[0].dataset['webhookName']) {
|
||||||
|
sel
|
||||||
|
.closest('div.reminderContent')
|
||||||
|
.querySelector('input.discord-username')
|
||||||
|
.value = sel.selectedOptions[0].dataset['webhookName'];
|
||||||
|
} else {
|
||||||
|
sel
|
||||||
|
.closest('div.reminderContent')
|
||||||
|
.querySelector('input.discord-username')
|
||||||
|
.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_guild_pane() {
|
||||||
|
document.querySelectorAll('select.channel-selector option').forEach((opt) => opt.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetch_roles(guild_id) {
|
||||||
|
fetch(`/dashboard/api/guild/${guild_id}/roles`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
show_error(data.error);
|
||||||
|
} else {
|
||||||
|
for (let role of data) {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetch_reminders(guild_id) {
|
||||||
|
// fetch dm reminders instead
|
||||||
|
if (guild_id === undefined) {
|
||||||
|
const $reminderBox = document.querySelector('div#personalReminders');
|
||||||
|
|
||||||
|
// reset div contents
|
||||||
|
$reminderBox.innerHTML = '';
|
||||||
|
|
||||||
|
// fetch reminders
|
||||||
|
fetch('dashboard/api/user/reminders')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
show_error(data.error);
|
||||||
|
} else {
|
||||||
|
const $template = document.querySelector('template#personalReminder');
|
||||||
|
|
||||||
|
for (let reminder of data) {
|
||||||
|
let newFrame = $template.content.cloneNode(true);
|
||||||
|
|
||||||
|
for (let prop in reminder) {
|
||||||
|
if (reminder.hasOwnProperty(prop) && reminder[prop] !== null) {
|
||||||
|
let $input = newFrame.querySelector(`*[name="${prop}"]`);
|
||||||
|
let $image = newFrame.querySelector(`img.${prop}`);
|
||||||
|
|
||||||
|
if ($input !== null) {
|
||||||
|
$input.value = reminder[prop];
|
||||||
|
} else if ($image !== null) {
|
||||||
|
$image.src = reminder[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reminderBox.append(newFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const $reminderBox = document.querySelector('div#guildReminders');
|
||||||
|
|
||||||
|
// reset div contents
|
||||||
|
$reminderBox.innerHTML = '';
|
||||||
|
|
||||||
|
// fetch reminders
|
||||||
|
fetch(`dashboard/api/guild/${guild_id}/reminders`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
show_error(data.error);
|
||||||
|
} else {
|
||||||
|
const $template = document.querySelector('template#personalReminder');
|
||||||
|
|
||||||
|
for (let reminder of data) {
|
||||||
|
let newFrame = $template.content.cloneNode(true);
|
||||||
|
|
||||||
|
for (let prop in reminder) {
|
||||||
|
if (reminder.hasOwnProperty(prop) && reminder[prop] !== null) {
|
||||||
|
let $input = newFrame.querySelector(`*[name="${prop}"]`);
|
||||||
|
let $image = newFrame.querySelector(`img.${prop}`);
|
||||||
|
|
||||||
|
if ($input !== null) {
|
||||||
|
$input.value = reminder[prop];
|
||||||
|
} else if ($image !== null) {
|
||||||
|
$image.src = reminder[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reminderBox.appendChild(newFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_error(error) {
|
||||||
|
document.getElementById('errors').querySelector('span.error-message').textContent = error;
|
||||||
|
document.getElementById('errors').classList.add('is-active');
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
document.getElementById('errors').classList.remove('is-active');
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_times() {
|
||||||
|
document.querySelectorAll('span.set-timezone').forEach((element) => {
|
||||||
|
element.textContent = timezone;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('span.set-time').forEach((element) => {
|
||||||
|
element.textContent = luxon.DateTime.now().setZone(timezone).toFormat('HH:mm');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('span.browser-timezone').forEach((element) => {
|
||||||
|
element.textContent = browserTimezone;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('span.browser-time').forEach((element) => {
|
||||||
|
element.textContent = luxon.DateTime.now().toFormat('HH:mm');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('span.bot-timezone').forEach((element) => {
|
||||||
|
element.textContent = botTimezone;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('span.bot-time').forEach((element) => {
|
||||||
|
element.textContent = luxon.DateTime.now().setZone(botTimezone).toFormat('HH:mm');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setInterval(() => {
|
||||||
|
update_times();
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
let colorPicker = new iro.ColorPicker('#colorpicker');
|
||||||
|
let $discordFrame;
|
||||||
|
const $colorPickerModal = document.querySelector('div#pickColorModal');
|
||||||
|
const $colorPickerInput = $colorPickerModal.querySelector('input');
|
||||||
|
|
||||||
|
let timezone = luxon.DateTime.now().zone.name;
|
||||||
|
const browserTimezone = luxon.DateTime.now().zone.name;
|
||||||
|
let botTimezone = 'UTC';
|
||||||
|
|
||||||
|
document.getElementById('set-bot-timezone').addEventListener('click', () => {
|
||||||
|
timezone = botTimezone;
|
||||||
|
update_times();
|
||||||
|
});
|
||||||
|
document.getElementById('set-browser-timezone').addEventListener('click', () => {
|
||||||
|
timezone = browserTimezone;
|
||||||
|
update_times();
|
||||||
|
});
|
||||||
|
document.getElementById('update-bot-timezone').addEventListener('click', () => {
|
||||||
|
timezone = browserTimezone;
|
||||||
|
fetch('/dashboard/api/user', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({timezone: timezone})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
show_error(data.error)
|
||||||
|
} else {
|
||||||
|
botTimezone = browserTimezone;
|
||||||
|
update_times();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$colorPickerInput.value = colorPicker.color.hexString;
|
||||||
|
|
||||||
|
$colorPickerInput.addEventListener('input', () => {
|
||||||
|
if (/^#[0-9a-fA-F]{6}$/.test($colorPickerInput.value) === true) {
|
||||||
|
colorPicker.color.hexString = $colorPickerInput.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
colorPicker.on('color:change', function (color) {
|
||||||
|
$colorPickerInput.value = color.hexString;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.discord-embed').forEach((element) => {
|
||||||
|
element.addEventListener('click', (e) => {
|
||||||
|
if (e.offsetX < parseInt(window.getComputedStyle(element).borderLeftWidth)) {
|
||||||
|
$discordFrame = element;
|
||||||
|
$colorPickerModal.classList.toggle('is-active');
|
||||||
|
colorPicker.color.rgbString = window.getComputedStyle($discordFrame).borderLeftColor;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.set-color').forEach((element) => {
|
||||||
|
element.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
$discordFrame = element.closest('div.reminderContent').querySelector('div.discord-embed');
|
||||||
|
$colorPickerModal.classList.toggle('is-active');
|
||||||
|
colorPicker.color.rgbString = window.getComputedStyle($discordFrame).borderLeftColor;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
$colorPickerModal.querySelector('button.is-success').addEventListener('click', () => {
|
||||||
|
$discordFrame.style.borderLeftColor = colorPicker.color.rgbString;
|
||||||
|
|
||||||
|
$colorPickerModal.classList.remove('is-active')
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('a.show-modal').forEach((element) => {
|
||||||
|
element.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById(element.dataset['modal']).classList.toggle('is-active');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.navbar-burger').forEach(el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
const target = el.dataset.target;
|
||||||
|
const $target = document.getElementById(target);
|
||||||
|
|
||||||
|
el.classList.toggle('is-active');
|
||||||
|
$target.classList.toggle('is-active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('/dashboard/api/user')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
show_error(data.error);
|
||||||
|
} else {
|
||||||
|
document.querySelectorAll('a.switch-pane').forEach((element) => {
|
||||||
|
element.innerHTML = element.innerHTML.replace('%username%', data.name);
|
||||||
|
|
||||||
|
element.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
switch_pane(element.dataset['pane']);
|
||||||
|
|
||||||
|
element.classList.add('is-active');
|
||||||
|
|
||||||
|
resize_textareas();
|
||||||
|
|
||||||
|
document.querySelectorAll('p.pageTitle').forEach((el) => {
|
||||||
|
el.textContent = 'Your Reminders';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.timezone !== null) {
|
||||||
|
botTimezone = data.timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_times();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('/dashboard/api/user/guilds')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
show_error(data.error);
|
||||||
|
} else {
|
||||||
|
const $template = document.getElementById('guildListEntry');
|
||||||
|
|
||||||
|
for (let guild of data) {
|
||||||
|
document.querySelectorAll('.guildList').forEach((element) => {
|
||||||
|
const $clone = $template.content.cloneNode(true);
|
||||||
|
const $anchor = $clone.querySelector('a');
|
||||||
|
|
||||||
|
$anchor.innerHTML = $clone.querySelector('a').innerHTML.replace('%guildname%', guild.name);
|
||||||
|
$anchor.dataset['guild'] = guild.id;
|
||||||
|
$anchor.dataset['name'] = guild.name;
|
||||||
|
|
||||||
|
$anchor.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
switch_pane($anchor.dataset['pane']);
|
||||||
|
|
||||||
|
reset_guild_pane();
|
||||||
|
|
||||||
|
fetch_roles($anchor.dataset['guild']);
|
||||||
|
|
||||||
|
fetch(`/dashboard/api/guild/${$anchor.dataset['guild']}/channels`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
show_error(data.error);
|
||||||
|
} else {
|
||||||
|
document.querySelectorAll('select.channel-selector').forEach((el) => {
|
||||||
|
for (let channel of data) {
|
||||||
|
let newOption = document.createElement('option');
|
||||||
|
|
||||||
|
newOption.value = channel.id;
|
||||||
|
newOption.textContent = channel.name;
|
||||||
|
if (channel.webhook_avatar !== null) {
|
||||||
|
newOption.dataset['webhookAvatar'] = channel.webhook_avatar;
|
||||||
|
}
|
||||||
|
if (channel.webhook_name !== null) {
|
||||||
|
newOption.dataset['webhookName'] = channel.webhook_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.appendChild(newOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
update_select(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(`/dashboard/api/guild/${$anchor.dataset['guild']}/reminders`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
document.querySelectorAll('p.pageTitle').forEach((el) => {
|
||||||
|
el.textContent = $anchor.dataset['name'] + ' Reminders';
|
||||||
|
});
|
||||||
|
document.querySelectorAll('select.channel-selector').forEach((el) => {
|
||||||
|
el.addEventListener('change', (e) => {
|
||||||
|
update_select(e.target);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
$anchor.classList.add('is-active');
|
||||||
|
resize_textareas();
|
||||||
|
});
|
||||||
|
|
||||||
|
element.append($clone);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('textarea.autoresize').forEach((element) => {
|
||||||
|
element.addEventListener('input', () => {
|
||||||
|
element.style.height = "";
|
||||||
|
element.style.height = element.scrollHeight + 3 + "px";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let $img;
|
||||||
|
const $urlModal = document.querySelector('div#addImageModal');
|
||||||
|
const $urlInput = $urlModal.querySelector('input');
|
||||||
|
|
||||||
|
$urlModal.querySelector('button.is-success').addEventListener('click', () => {
|
||||||
|
$img.src = $urlInput.value;
|
||||||
|
|
||||||
|
$urlInput.value = '';
|
||||||
|
$urlModal.classList.remove('is-active')
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('button.close-modal').forEach((element) => {
|
||||||
|
element.addEventListener('click', () => {
|
||||||
|
let $modal = element.closest('div.modal');
|
||||||
|
|
||||||
|
$urlInput.value = '';
|
||||||
|
|
||||||
|
$modal.classList.remove('is-active')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.customizable').forEach((element) => {
|
||||||
|
element.querySelector('a').addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
$img = element.querySelector('img');
|
||||||
|
|
||||||
|
$urlModal.classList.toggle('is-active')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('a.icon-toggle').forEach((element) => {
|
||||||
|
element.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
element.classList.toggle('is-active');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let $showButton = document.querySelector('button#showReminderCreator');
|
||||||
|
|
||||||
|
$showButton.addEventListener('click', () => {
|
||||||
|
$showButton.querySelector('span.icon i').classList.toggle('fa-chevron-right');
|
||||||
|
$showButton.querySelector('span.icon i').classList.toggle('fa-chevron-down');
|
||||||
|
document.querySelector('div#reminderCreator').classList.toggle('is-hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.discord-field-title').forEach((element) => {
|
||||||
|
const $template = document.querySelector('template#embedFieldTemplate');
|
||||||
|
const $complement = element.parentElement.querySelector('.discord-field-value');
|
||||||
|
|
||||||
|
// when the user clicks out of the field title and if the field title/value are empty, remove the field
|
||||||
|
element.addEventListener('blur', () => {
|
||||||
|
if (element.value === '' && $complement.value === '' && element.parentElement.nextElementSibling !== null) {
|
||||||
|
element.parentElement.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$complement.addEventListener('blur', () => {
|
||||||
|
if (element.value === '' && $complement.value === '' && element.parentElement.nextElementSibling !== null) {
|
||||||
|
element.parentElement.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// when the user inputs into the end field, create a new field after it
|
||||||
|
element.addEventListener('input', () => {
|
||||||
|
if (element.value !== '' && $complement.value !== '' && element.parentElement.nextElementSibling === null) {
|
||||||
|
const $clone = $template.content.cloneNode(true);
|
||||||
|
element.parentElement.parentElement.append($clone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$complement.addEventListener('input', () => {
|
||||||
|
if (element.value !== '' && $complement.value !== '' && element.parentElement.nextElementSibling === null) {
|
||||||
|
const $clone = $template.content.cloneNode(true);
|
||||||
|
element.parentElement.parentElement.append($clone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMNodeInserted', () => {
|
||||||
|
document.querySelectorAll('div.mobile-sidebar a').forEach((element) => {
|
||||||
|
element.addEventListener('click', (e) => {
|
||||||
|
document.getElementById('mobileSidebar').classList.remove('is-active');
|
||||||
|
document.querySelectorAll('.navbar-burger').forEach((el) => {
|
||||||
|
el.classList.remove('is-active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.discord-field-title').forEach((element) => {
|
||||||
|
const $template = document.querySelector('template#embedFieldTemplate');
|
||||||
|
const $complement = element.parentElement.querySelector('.discord-field-value');
|
||||||
|
|
||||||
|
// when the user clicks out of the field title and if the field title/value are empty, remove the field
|
||||||
|
element.addEventListener('blur', () => {
|
||||||
|
if (element.value === '' && $complement.value === '' && element.parentElement.nextElementSibling !== null) {
|
||||||
|
element.parentElement.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$complement.addEventListener('blur', () => {
|
||||||
|
if (element.value === '' && $complement.value === '' && element.parentElement.nextElementSibling !== null) {
|
||||||
|
element.parentElement.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// when the user inputs into the end field, create a new field after it
|
||||||
|
element.addEventListener('input', () => {
|
||||||
|
if (element.value !== '' && $complement.value !== '' && element.parentElement.nextElementSibling === null) {
|
||||||
|
const $clone = $template.content.cloneNode(true);
|
||||||
|
element.parentElement.parentElement.append($clone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$complement.addEventListener('input', () => {
|
||||||
|
if (element.value !== '' && $complement.value !== '' && element.parentElement.nextElementSibling === null) {
|
||||||
|
const $clone = $template.content.cloneNode(true);
|
||||||
|
element.parentElement.parentElement.append($clone);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
resize_textareas();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
web/templates/errors/401.html.tera
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block init %}
|
||||||
|
{% set title = "401 Not Authorized" %}
|
||||||
|
|
||||||
|
{% set show_login = True %}
|
||||||
|
|
||||||
|
{% set page_title = "Not Authorized" %}
|
||||||
|
{% set page_subtitle = "You must be logged in to access this page, if it exists." %}
|
||||||
|
{% endblock %}
|
10
web/templates/errors/403.html.tera
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block init %}
|
||||||
|
{% set title = "403 Forbidden" %}
|
||||||
|
|
||||||
|
{% set show_contact = True %}
|
||||||
|
|
||||||
|
{% set page_title = "Forbidden" %}
|
||||||
|
{% set page_subtitle = "You currently cannot access this page, if it exists. Sorry." %}
|
||||||
|
{% endblock %}
|
10
web/templates/errors/404.html.tera
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block init %}
|
||||||
|
{% set title = "404 File Not Found" %}
|
||||||
|
|
||||||
|
{% set show_contact = True %}
|
||||||
|
|
||||||
|
{% set page_title = "File Not Found" %}
|
||||||
|
{% set page_subtitle = "This page does not exist. Sorry." %}
|
||||||
|
{% endblock %}
|
9
web/templates/errors/500.html.tera
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block init %}
|
||||||
|
{% set title = "500 Internal Server Error" %}
|
||||||
|
{% set show_contact = True %}
|
||||||
|
|
||||||
|
{% set page_title = "An Error Has Occurred" %}
|
||||||
|
{% set page_subtitle = "A server error has occurred. Please contact me and I will try and resolve this" %}
|
||||||
|
{% endblock %}
|
14
web/templates/help.html.tera
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% set title = "Commands" %}
|
||||||
|
{% set page_title = "Commands" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">Information Commands</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
65
web/templates/index.html.tera
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block init %}
|
||||||
|
{% set title = "Home" %}
|
||||||
|
|
||||||
|
{% set page_title = "Reminder Bot" %}
|
||||||
|
{% set page_subtitle = "Powerful Discord Reminders" %}
|
||||||
|
{% set page_emoji = "fa-hourglass-half" %}
|
||||||
|
{% set show_invite = true %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="inset-content" >
|
||||||
|
<div class="tile is-ancestor">
|
||||||
|
<div class="tile is-parent">
|
||||||
|
<article class="tile is-child notification">
|
||||||
|
<p class="title">Slash-command Ready <svg aria-hidden="false" width="28" height="28" viewBox="0 0 24 24"><path fill="#777" fill-rule="evenodd" clip-rule="evenodd" d="M5 3C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3H5ZM16.8995 8.41419L15.4853 6.99998L7 15.4853L8.41421 16.8995L16.8995 8.41419Z"></path></svg></p>
|
||||||
|
<p class="subtitle">Set reminders easily and quickly from anywhere</p>
|
||||||
|
<figure class="image">
|
||||||
|
<img class="rounded-corners" src="/static/img/slash-commands.png" alt="Discord slash commands demonstration">
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class="tile is-parent">
|
||||||
|
<article class="tile is-child notification">
|
||||||
|
<p class="title">Advanced Options <span class="icon"><i class="fad fa-palette"></i></span></p>
|
||||||
|
<p class="subtitle">Decorate your announcements with our web dashboard</p>
|
||||||
|
<figure class="image">
|
||||||
|
<img class="rounded-corners" src="/static/img/tournament-demo.png" alt="Discord slash commands demonstration">
|
||||||
|
</figure>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class="tile is-parent is-vertical">
|
||||||
|
<article class="tile is-child notification">
|
||||||
|
<p class="title">Unlimited Reminders <span class="icon"><i class="far fa-infinity"></i></span></p>
|
||||||
|
<p class="subtitle">Never forget a thing</p>
|
||||||
|
</article>
|
||||||
|
<article class="tile is-child notification">
|
||||||
|
<p class="title">Repeating Reminders <span class="icon"><i class="fas fa-repeat"></i></span></p>
|
||||||
|
<p class="subtitle">Available to <a href="https://patreon.com/jellywx"><span class="patreon-color">Patreon <span class="icon"><i class="fab fa-patreon"></i></span></span></a> subscribers at <strong>$2/month</strong></p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="hero is-small">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container has-text-centered">
|
||||||
|
<p class="title">Ready to go?</p>
|
||||||
|
<p class="content">
|
||||||
|
Add the bot to get started!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-foot has-text-centered">
|
||||||
|
<a class="button is-size-6 is-rounded is-success" href="https://invite.reminder-bot.com">
|
||||||
|
<p class="is-size-6">
|
||||||
|
Add Now <span class="icon"><i class="fas fa-chevron-right"></i></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
21
web/templates/privacy.html.tera
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block init %}
|
||||||
|
{% set title = "Privacy Policy" %}
|
||||||
|
|
||||||
|
{% set page_title = "Privacy Policy" %}
|
||||||
|
{% set page_subtitle = "" %}
|
||||||
|
{% set page_emoji = "" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">Privacy Policy</h2>
|
||||||
|
<p class="is-size-5 pl-6">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
191
web/templates/reminder_dashboard/guild_reminder.html.tera
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<div class="columns reminderContent">
|
||||||
|
<div class="column discord-frame">
|
||||||
|
<div class="">
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left">
|
||||||
|
<p class="image is-32x32 customizable">
|
||||||
|
<a>
|
||||||
|
<img class="is-rounded discord-avatar" src="">
|
||||||
|
</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>
|
||||||
|
<input class="discord-username message-input" placeholder="Username Override"
|
||||||
|
maxlength="32" id="reminderUsername" name="username">
|
||||||
|
</div>
|
||||||
|
<label class="is-sr-only" for="messageContent">Message</label>
|
||||||
|
<textarea class="message-input autoresize discord-content"
|
||||||
|
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" src="">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="b">
|
||||||
|
<label class="is-sr-only" for="embedAuthor">Embed Author</label>
|
||||||
|
<textarea
|
||||||
|
class="discord-embed-author message-input 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 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 "
|
||||||
|
placeholder="Embed Description..."
|
||||||
|
maxlength="4096" 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 "
|
||||||
|
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 "
|
||||||
|
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="" src="" alt="Square thumbnail embedded image">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="image is-400x300 customizable">
|
||||||
|
<a>
|
||||||
|
<img class="" src="" alt="Large embedded image">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="embed-footer-box">
|
||||||
|
<p class="image is-20x20 customizable">
|
||||||
|
<a>
|
||||||
|
<img class="is-rounded " src="" alt="Footer profile-like image">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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_author" rows="1"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="label sr-only">Reminder Name</label>
|
||||||
|
<input class="input" type="text" placeholder="Reminder Name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Channel</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select id="channelOption" name="channel" class="channel-selector">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="checkbox">
|
||||||
|
Enable TTS
|
||||||
|
<input type="checkbox">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="checkbox">
|
||||||
|
Pin Reminder
|
||||||
|
<input type="checkbox">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="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>
|
||||||
|
{% if creating %}
|
||||||
|
<button class="button is-outlined">
|
||||||
|
Load Template
|
||||||
|
</button>
|
||||||
|
<button class="button is-success is-outlined" id="createTemplate">
|
||||||
|
Create Template
|
||||||
|
</button>
|
||||||
|
<button class="button is-success" id="createReminder">
|
||||||
|
Create Reminder
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="button is-primary" disabled>
|
||||||
|
Saved!
|
||||||
|
</button>
|
||||||
|
<button class="button is-warning">
|
||||||
|
Disable
|
||||||
|
</button>
|
||||||
|
<button class="button is-danger">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
186
web/templates/reminder_dashboard/personal_reminder.html.tera
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<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>
|
@ -0,0 +1,22 @@
|
|||||||
|
<div style="margin: 0 12px 12px 12px;">
|
||||||
|
<div class="create-reminder">
|
||||||
|
<button class="button is-rounded is-light" id="showReminderCreator">
|
||||||
|
<strong>Create Reminder <span class="icon left-pad"><i class="fas fa-chevron-down"></i></span></strong>
|
||||||
|
</button>
|
||||||
|
<div id="reminderCreator">
|
||||||
|
{% set creating = true %}
|
||||||
|
{% include "reminder_dashboard/guild_reminder" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons has-addons" style="margin-top: 1rem;">
|
||||||
|
<button class="button is-static">Order By</button>
|
||||||
|
<button class="button" disabled>Time</button>
|
||||||
|
<button class="button">Name</button>
|
||||||
|
<button class="button">Channel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="guildReminders">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
13
web/templates/return.html.tera
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Reminder Bot | Redirecting...</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a href="/{{ to }}">Press here if you aren't redirected</a>
|
||||||
|
<script>
|
||||||
|
document.location = "/{{ to }}";
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
55
web/templates/terms.html.tera
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block init %}
|
||||||
|
{% set title = "Terms of Service" %}
|
||||||
|
|
||||||
|
{% set page_title = "Terms of Service" %}
|
||||||
|
{% set page_subtitle = "" %}
|
||||||
|
{% set page_emoji = "" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">Outline</h2>
|
||||||
|
<p class="is-size-5 pl-6">
|
||||||
|
The Terms of Service apply whenever you use <strong>Reminder Bot</strong> and the
|
||||||
|
<strong>JellyWX's Home</strong> Discord server.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Violating the Terms of Service may result in receiving a permanent ban from the Discord server,
|
||||||
|
permanent restriction on your usage of Reminder Bot, or removal of some or all of your content on
|
||||||
|
Reminder Bot or the Discord server.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
The Terms of Service may be updated at any time, and should be considered a guideline for appropriate
|
||||||
|
behaviour.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">Reminder Bot</h2>
|
||||||
|
<ul class="is-size-5 pl-6">
|
||||||
|
<li>Reasonably disclose potential exploits or bugs to me by email or by Discord private message</li>
|
||||||
|
<li>Do not use the bot to harass other Discord users</li>
|
||||||
|
<li>Do not use the bot to send more than 30 messages during a 60 second period</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">JellyWX's Home</h2>
|
||||||
|
<ul class="is-size-5 pl-6">
|
||||||
|
<li>Do not discuss politics, harass other users, or use language intended to upset other users</li>
|
||||||
|
<li>Do not send malicious links</li>
|
||||||
|
<li>Do not advertise</li>
|
||||||
|
<li>Do not send unwarranted direct messages</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|