Initial commit

This commit is contained in:
jude 2023-10-28 14:50:37 +01:00
commit 8d384c9e50
91 changed files with 73289 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

2
.prettierrc.toml Normal file
View File

@ -0,0 +1,2 @@
printWidth = 100
tabWidth = 4

62
index.html Normal file
View File

@ -0,0 +1,62 @@
<!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"/>
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> -->
<!-- favicon -->
<link rel="apple-touch-icon" sizes="180x180"
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/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">
</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 width="28px" height="28px" src="/static/img/logo_nobg.webp" alt="Reminder Bot Logo">
</figure>
</a>
<p class="navbar-item pageTitle"></p>
<a
role="button"
class="dashboard-burger 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>
<div id="app"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

4697
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "example",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"air-datepicker": "^3.4.0",
"axios": "^1.5.1",
"bulma": "^0.9.4",
"luxon": "^3.4.3",
"preact": "^10.13.1",
"react-query": "^3.39.3",
"tributejs": "^5.1.3",
"wouter": "^2.12.1"
},
"devDependencies": {
"@preact/preset-vite": "^2.5.0",
"@types/luxon": "^3.3.2",
"eslint": "^8.50.0",
"eslint-config-preact": "^1.3.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vite": "^4.3.2"
}
}

1
public/static/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View 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
public/static/css/fa.css Normal file

File diff suppressed because it is too large Load Diff

View File

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

888
public/static/css/style.css Normal file
View File

@ -0,0 +1,888 @@
* {
font-family: "Ubuntu Bold", "Ubuntu", sans-serif;
}
button {
font-weight: 700;
}
/* override styles for when the div is collapsed */
div.reminderContent.is-collapsed .column.discord-frame {
display: none;
}
div.reminderContent.is-collapsed .column.settings {
display: none;
}
div.reminderContent.is-collapsed .reminder-settings {
margin-bottom: 0;
}
div.reminderContent.is-collapsed .button-row {
display: none;
}
div.reminderContent.is-collapsed .button-row-edit {
display: none;
}
div.reminderContent.is-collapsed .reminder-topbar {
padding-bottom: 0;
}
div.reminderContent.is-collapsed .invert-collapses {
display: inline-flex;
}
div.reminderContent .invert-collapses {
display: none;
}
div.reminderContent.is-collapsed input[name="name"] {
display: inline-flex;
flex-grow: 1;
border: none;
background: none;
box-shadow: none;
opacity: 1;
}
div.reminderContent.is-collapsed .hide-box {
display: inline-flex;
}
div.reminderContent.is-collapsed .hide-box i {
transform: rotate(90deg);
}
/* END */
/* dashboard styles */
.hide-box {
border: none;
background: none;
}
.hide-box:focus {
outline: none;
box-shadow: none !important;
}
.channel-bar {
display: flex;
justify-content: center;
flex-direction: column;
font-weight: bold;
}
button.inline-btn {
height: 100%;
padding: 5px;
}
button.change-color {
position: absolute;
left: calc(-1rem - 40px);
}
button.disable-enable[data-action="enable"]:after {
content: "Enable";
}
button.disable-enable[data-action="disable"]:after {
content: "Disable";
}
.media-content {
overflow-x: visible;
}
div.discord-embed {
position: relative;
}
div.split-controls {
display: flex;
flex-direction: column;
justify-content: space-between;
flex-grow: 2;
}
.reminder-topbar > div {
padding-left: 6px;
padding-right: 6px;
}
.settings {
display: flex;
flex-direction: column;
}
.name-bar {
flex-grow: 1;
flex-shrink: 1;
}
.hide-button-bar {
flex-grow: 0;
flex-shrink: 0;
}
.patreon-only {
padding-bottom: 16px;
}
.tts-row {
padding-bottom: 10px;
}
.reminder-topbar {
display: flex;
margin-bottom: 0 !important;
}
.reminder-settings {
margin-top: 0 !important;
}
.reminder-settings > .column {
flex-grow: 0;
flex-shrink: 0;
flex-basis: 50%;
}
div.reminderContent {
margin-top: 10px;
margin-bottom: 10px;
padding: 14px;
background-color: #f5f5f5;
border-radius: 8px;
}
/* Interval inputs */
div.interval-group {
height: unset !important;
}
div.interval-group .clear:focus {
outline: none;
box-shadow: none !important;
}
div.interval-group .no-break {
text-wrap: avoid;
white-space: nowrap;
}
div.interval-group .clear {
border: none;
background: none;
padding: 1px;
margin-right: -3px;
}
div.interval-group > .interval-group-left input {
-webkit-appearance: none;
border-style: none;
background-color: #eee;
font-size: 1rem;
font-family: monospace;
}
div.interval-group > .interval-group-left input.w2 {
width: 3ch;
}
div.interval-group > .interval-group-left input.w3 {
width: 3ch;
}
div.interval-group {
display: flex;
flex-direction: row;
justify-content: space-between;
}
/* !Interval inputs */
.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;
}
p.pageTitle {
margin-left: 12px;
}
#welcome > div {
height: 100%;
padding-top: 30vh;
}
div#pageNavbar {
background-color: #363636;
}
div#pageNavbar a {
color: #fff;
text-align: center;
}
.navbar-burger {
flex-shrink: 0;
}
.navbar-item.pageTitle {
flex-shrink: 1;
white-space: nowrap;
overflow: hidden;
}
.dashboard-burger, .dashboard-burger:active, .dashboard-burger.is-active {
background-color: #adc99c !important;
border-radius: 14px;
padding: 6px;
background-clip: content-box;
}
div#pageNavbar a:hover {
background-color: #4a4a4a;
}
img.rounded-corners {
border-radius: 12px;
}
div.brand {
text-align: center;
height: 52px;
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;
}
ul.guildList {
flex-grow: 1;
flex-shrink: 1;
overflow: auto;
}
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
flex-shrink: 0;
flex-grow: 0;
position: fixed;
bottom: 0;
width: 226px;
}
div.dashboard-sidebar svg {
flex-shrink: 0;
}
div.mobile-sidebar {
z-index: 100;
min-height: 100vh;
position: absolute;
top: 0;
display: none;
flex-direction: column;
}
#expandAll {
width: 60px;
}
div.mobile-sidebar .aside-footer {
margin-top: auto;
}
div.mobile-sidebar.is-active {
display: flex;
}
aside.menu {
display: flex;
flex-direction: column;
flex-grow: 1;
}
div.dashboard-frame {
min-height: 100vh;
margin-bottom: 0 !important;
}
.embed-field-box[data-inlined="0"] .inline-btn > i {
transform: rotate(90deg);
}
.embed-field-box[data-inlined="0"] {
min-width: 100%;
}
.embed-field-box[data-inlined="1"] {
min-width: auto;
}
.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%;
}
input.default-width {
width: initial;
}
.message-input:placeholder-shown {
font-style: italic;
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%;
height: 100px;
}
.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;
}
.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;
margin-right: 4px;
}
.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;
}
.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;
}
.channel-selector {
width: 100%;
}
.select {
width: 100%;
}
li.highlight {
margin-bottom: 0 !important;
}
.button-row {
display: flex;
}
.button-row .button-row-reminder {
flex-grow: 0;
padding: 2px;
}
.button-row-template {
display: flex;
flex-grow: 1;
justify-content: space-between;
}
.button-row .button-row-template > div {
padding: 2px;
}
@media only screen and (max-width: 1023px) {
p.title.pageTitle {
display: none;
}
.dashboard-frame {
margin-top: 4rem !important;
}
.customizable.thumbnail img {
width: 60px;
height: 60px;
}
}
@media only screen and (max-width: 768px) {
.button-row {
display: flex;
flex-direction: column;
}
.button-row .button-row-reminder {
width: 100%;
}
.button-row .button-row-template > div {
flex-basis: 0;
flex-grow: 1;
}
.button-row button {
width: 100%;
}
.reminder-settings {
margin-bottom: 0 !important;
}
.tts-row {
padding-bottom: 0;
}
}
/* loader */
#loader {
position: fixed;
top: 0;
background-color: rgba(255, 255, 255, 0.8);
width: 100vw;
z-index: 999;
}
#loader .title {
font-size: 6rem;
}
/* END */
div.reminderError {
margin: 10px;
padding: 14px;
background-color: #f5f5f5;
border-radius: 8px;
}
div.reminderError .errorHead {
display: flex;
flex-direction: row;
}
div.reminderError .errorIcon {
padding: 8px;
border-radius: 4px;
margin-right: 12px;
}
div.reminderError .errorIcon .fas {
display: none
}
div.reminderError[data-case="deleted"] .errorIcon {
background-color: #e7e5e4;
}
div.reminderError[data-case="failed"] .errorIcon {
background-color: #fecaca;
}
div.reminderError[data-case="sent"] .errorIcon {
background-color: #d9f99d;
}
div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
display: block;
}
div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
display: block;
}
div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
display: block;
}
div.reminderError .errorHead .reminderName {
font-size: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
color: rgb(54, 54, 54);
flex-grow: 1;
}
div.reminderError .errorHead .reminderTime {
font-size: 1rem;
display: flex;
flex-direction: column;
flex-shrink: 1;
justify-content: center;
color: rgb(54, 54, 54);
background-color: #ffffff;
padding: 8px;
border-radius: 4px;
border-color: #e5e5e5;
border-width: 1px;
border-style: solid;
}
div.reminderError .reminderMessage {
font-size: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
color: rgb(54, 54, 54);
flex-grow: 1;
font-style: italic;
}
/* other stuff */
.half-rem {
width: 0.5rem;
}
.pad-left {
width: 12px;
}
#dead {
display: none;
}
.colorpicker-container {
display: flex;
justify-content: center;
}
.create-reminder {
margin: 0 12px 12px 12px;
}
.button.is-success:not(.is-outlined) {
color: white;
}
.button.is-outlined.is-success {
background-color: white;
}
a.switch-pane {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.guild-submenu {
display: none;
}
.guild-submenu li {
font-size: 0.8rem;
}
a.switch-pane.is-active ~ .guild-submenu {
display: block;
}
.feedback {
background-color: #5865F2;
}
.is-locked {
pointer-events: none;
}
.is-locked > :not(.patreon-invert) {
opacity: 0.4;
}
.is-locked .patreon-invert {
display: block;
}
.patreon-invert {
display: none;
}
.is-locked .foreground {
pointer-events: auto;
}
.is-locked .field:last-of-type {
display: none;
}
.stat-row {
display: flex;
flex-direction: row;
}
.stat-box {
flex-grow: 1;
border-radius: 6px;
background-color: #fcfcfc;
border-color: #efefef;
border-style: solid;
border-width: 1px;
margin: 4px;
padding: 4px;
}
.figure {
text-align: center;
}
.figure-num {
font-size: 2rem;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
public/static/img/bg.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

BIN
public/static/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

131
public/static/js/admin.js Normal file
View File

@ -0,0 +1,131 @@
document.addEventListener("DOMContentLoaded", () => {
fetch("/admin/data")
.then((resp) => resp.json())
.then((data) => {
document.querySelector("#backlog").textContent = data.backlog;
document.querySelector("#reminders").textContent = data.count.reminders;
document.querySelector("#intervals").textContent = data.count.intervals;
let historySent = data.historyLong.sent.reduce(
(iv, frame) => iv + frame.count,
0
);
let historyFailed = data.historyLong.failed.reduce(
(iv, frame) => iv + frame.count,
0
);
let rate = historyFailed / (historySent + historyFailed);
let formatted = Math.round(rate * 10000) / 100;
document.querySelector("#historySent").textContent = historySent;
document.querySelector("#historyFailed").textContent = historyFailed;
document.querySelector("#failRate").textContent = `${formatted}%`;
new Chart(document.getElementById("schedule"), {
type: "bar",
data: {
labels: [
...data.scheduleShort.once,
...data.scheduleShort.interval,
].map((row) => luxon.DateTime.fromISO(row.time_key)),
datasets: [
{
label: "Reminders",
data: data.scheduleShort.once.map((row) => row.count),
},
{
label: "Intervals",
data: data.scheduleShort.interval.map((row) => row.count),
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
type: "time",
time: {
unit: "minute",
},
},
y: {
stacked: true,
},
},
},
});
new Chart(document.getElementById("scheduleLong"), {
type: "bar",
data: {
labels: [
...data.scheduleLong.once,
...data.scheduleLong.interval,
].map((row) => luxon.DateTime.fromISO(row.time_key)),
datasets: [
{
label: "Reminders",
data: data.scheduleLong.once.map((row) => row.count),
},
{
label: "Intervals",
data: data.scheduleLong.interval.map((row) => row.count),
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
type: "time",
time: {
unit: "day",
},
},
y: {
stacked: true,
},
},
},
});
new Chart(document.getElementById("historyLong"), {
type: "bar",
data: {
labels: [...data.historyLong.sent, ...data.historyLong.failed].map(
(row) => luxon.DateTime.fromISO(row.time_key)
),
datasets: [
{
label: "Success",
data: data.historyLong.sent.map((row) => row.count),
},
{
label: "Fail",
data: data.historyLong.failed.map((row) => row.count),
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
type: "time",
time: {
unit: "day",
},
},
y: {
stacked: true,
},
},
},
});
});
});

20
public/static/js/chart.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
/*!
* chartjs-adapter-luxon v1.3.1
* https://www.chartjs.org
* (c) 2023 chartjs-adapter-luxon Contributors
* Released under the MIT license
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})}));

931
public/static/js/dtsel.js Normal file
View 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 = "&lt;";
row.children[2].innerHTML = "&gt;";
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},
}),
},
});
})();

View File

@ -0,0 +1,23 @@
function collapse_all() {
document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
el.classList.add("is-collapsed");
});
}
function expand_all() {
document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
el.classList.remove("is-collapsed");
});
}
const expandAll = document.querySelector("#expandAll");
expandAll.addEventListener("change", (ev) => {
if (ev.target.value === "expand") {
expand_all();
} else if (ev.target.value === "collapse") {
collapse_all();
}
ev.target.value = "";
});

View File

@ -0,0 +1,94 @@
function get_interval(element) {
let months = element.querySelector('input[name="interval_months"]').value;
let days = element.querySelector('input[name="interval_days"]').value;
let hours = element.querySelector('input[name="interval_hours"]').value;
let minutes = element.querySelector('input[name="interval_minutes"]').value;
let seconds = element.querySelector('input[name="interval_seconds"]').value;
return {
months: parseInt(months) || null,
days: parseInt(days) || null,
seconds:
(parseInt(hours) || 0) * 3600 +
(parseInt(minutes) || 0) * 60 +
(parseInt(seconds) || 0) || null,
};
}
function update_interval(element) {
let months = element.querySelector('input[name="interval_months"]');
let days = element.querySelector('input[name="interval_days"]');
let hours = element.querySelector('input[name="interval_hours"]');
let minutes = element.querySelector('input[name="interval_minutes"]');
let seconds = element.querySelector('input[name="interval_seconds"]');
let interval = get_interval(element);
if (interval.months === null && interval.days === null && interval.seconds === null) {
months.value = "";
days.value = "";
hours.value = "";
minutes.value = "";
seconds.value = "";
} else {
months.value = months.value.padStart(1, "0");
days.value = days.value.padStart(1, "0");
hours.value = hours.value.padStart(2, "0");
minutes.value = minutes.value.padStart(2, "0");
seconds.value = seconds.value.padStart(2, "0");
if (seconds.value >= 60) {
let quotient = Math.floor(seconds.value / 60);
let remainder = seconds.value % 60;
seconds.value = String(remainder).padStart(2, "0");
minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(
2,
"0"
);
}
if (minutes.value >= 60) {
let quotient = Math.floor(minutes.value / 60);
let remainder = minutes.value % 60;
minutes.value = String(remainder).padStart(2, "0");
hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0");
}
}
}
const $intervalGroup = document.querySelector(".interval-group");
document.querySelector(".interval-group").addEventListener(
"blur",
(ev) => {
if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
},
true
);
$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
$intervalGroup.querySelectorAll("input").forEach((el) => {
el.value = "";
});
});
document.addEventListener("remindersLoaded", (event) => {
for (reminder of event.detail) {
let $intervalGroup = reminder.node.querySelector(".interval-group");
$intervalGroup.addEventListener(
"blur",
(ev) => {
if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
},
true
);
$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
$intervalGroup.querySelectorAll("input").forEach((el) => {
el.value = "";
});
});
}
});

7
public/static/js/iro.js Normal file

File diff suppressed because one or more lines are too long

2
public/static/js/js.cookie.min.js vendored Normal file
View 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
public/static/js/luxon.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1087
public/static/js/main.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
const REPORTER_ID = crypto.randomUUID();
window.addEventListener("error", async (ev) => {
await fetch("/report", {
method: "POST",
body: JSON.stringify({
reporterId: REPORTER_ID,
url: window.location.href,
relativeTimestamp: ev.timeStamp,
errorMessage: ev.message,
errorLine: ev.lineno,
errorFile: ev.filename,
errorType: ev.type,
}),
});
});

70
public/static/js/sort.js Normal file
View File

@ -0,0 +1,70 @@
let guildReminders = document.querySelector("#guildReminders");
function sort_by(cond) {
if (cond === "channel") {
[...guildReminders.children]
.sort((a, b) => {
let channel1 = a.querySelector("select.channel-selector").value;
let channel2 = b.querySelector("select.channel-selector").value;
return channel1 > channel2 ? 1 : -1;
})
.forEach((node) => guildReminders.appendChild(node));
// go through and add channel categories
let currentChannelGroup = null;
for (let child of guildReminders.querySelectorAll("div.reminderContent")) {
let thisChannelGroup = child.querySelector("select.channel-selector").value;
if (currentChannelGroup !== thisChannelGroup) {
let newNode = document.createElement("div");
newNode.textContent =
"#" + channels.find((a) => a.id === thisChannelGroup).name;
newNode.classList.add("channel-tag");
guildReminders.insertBefore(newNode, child);
currentChannelGroup = thisChannelGroup;
}
}
} else {
// remove any channel tags if previous ordering was by channel
guildReminders.querySelectorAll("div.channel-tag").forEach((el) => {
el.remove();
});
if (cond === "time") {
[...guildReminders.children]
.sort((a, b) => {
let time1 = luxon.DateTime.fromISO(
a.querySelector('input[name="time"]').value
);
let time2 = luxon.DateTime.fromISO(
b.querySelector('input[name="time"]').value
);
return time1 > time2 ? 1 : -1;
})
.forEach((node) => guildReminders.appendChild(node));
} else {
[...guildReminders.children]
.sort((a, b) => {
let name1 = a.querySelector('input[name="name"]').value;
let name2 = b.querySelector('input[name="name"]').value;
return name1 > name2 ? 1 : -1;
})
.forEach((node) => guildReminders.appendChild(node));
}
}
}
const selector = document.querySelector("#orderBy");
selector.addEventListener("change", () => {
sort_by(selector.value);
});
document.addEventListener("remindersLoaded", () => {
sort_by(selector.value);
});

View File

@ -0,0 +1,57 @@
let timezone = luxon.DateTime.now().zone.name;
const browserTimezone = luxon.DateTime.now().zone.name;
let botTimezone = "UTC";
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);
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();
}
});
});

View File

@ -0,0 +1,20 @@
{
"name": "Reminder Bot Dashboard",
"short_name": "Reminders",
"start_url": "/dashboard",
"icons": [
{
"src": "/static/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

20
src/api.ts Normal file
View File

@ -0,0 +1,20 @@
import axios from "axios";
type UserInfo = {
name: string;
patreon: boolean;
timezone: string | null;
};
export type GuildInfo = {
id: string;
name: string;
};
export function fetchUserInfo(): Promise<UserInfo> {
return axios.get("/api/user").then((resp) => resp.data) as Promise<UserInfo>;
}
export function fetchUserGuilds(): Promise<GuildInfo[]> {
return axios.get("/api/user/guilds").then((resp) => resp.data) as Promise<GuildInfo[]>;
}

View File

@ -0,0 +1,32 @@
import { Sidebar } from "../Sidebar";
import { QueryClient, QueryClientProvider } from "react-query";
import { Route, Router, Switch } from "wouter";
import { Welcome } from "../Welcome";
import { GuildReminders } from "../GuildReminders";
export function App() {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<>
<Router base={"/dashboard"}>
<div class="columns is-gapless dashboard-frame">
<Sidebar></Sidebar>
<div class="column is-main-content">
<Switch>
<Route
path={"/:guild/reminders"}
component={GuildReminders}
></Route>
<Route>
<Welcome></Welcome>
</Route>
</Switch>
</div>
</div>
</Router>
</>
</QueryClientProvider>
);
}

View File

@ -0,0 +1,7 @@
import { useParams } from "wouter";
export const GuildReminders = () => {
const params = useParams();
return <>{params.guild}</>;
};

View File

@ -0,0 +1,11 @@
export const Brand = () => (
<div class="brand">
<img
src="/static/img/logo_nobg.webp"
alt="Reminder bot logo"
width="52px"
height="52px"
class="dashboard-brand"
></img>
</div>
);

View File

@ -0,0 +1,5 @@
export const DesktopSidebar = ({ children }) => {
return (
<div class="column is-2 is-sidebar-menu dashboard-sidebar is-hidden-touch">{children}</div>
);
};

View File

@ -0,0 +1,28 @@
import { GuildInfo } from "../../api";
import { Link, useLocation } from "wouter";
type Props = {
guild: GuildInfo;
};
export const GuildEntry = ({ guild }: Props) => {
const [loc] = useLocation();
const currentId = loc.match(/^\/(?<id>\d+)/);
return (
<li>
<Link
class={guild.id === currentId.groups.id ? "is-active switch-pane" : "switch-pane"}
data-pane="guild"
data-guild={guild.id}
data-name={guild.name}
href={`/${guild.id}/reminders`}
>
<span class="icon">
<i class="fas fa-map-pin"></i>
</span>{" "}
<span class="guild-name">{guild.name}</span>
</Link>
</li>
);
};

View File

@ -0,0 +1,7 @@
export const MobileSidebar = ({ children }) => {
return (
<div class="dashboard-sidebar mobile-sidebar is-hidden-desktop" id="mobileSidebar">
{children}
</div>
);
};

View File

@ -0,0 +1,11 @@
export const Wave = () => (
<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>
);

View File

@ -0,0 +1,80 @@
import { useQuery } from "react-query";
import { DesktopSidebar } from "./DesktopSidebar";
import { MobileSidebar } from "./MobileSidebar";
import { Brand } from "./Brand";
import { Wave } from "./Wave";
import { GuildEntry } from "./GuildEntry";
import { fetchUserGuilds, GuildInfo } from "../../api";
import { QueryKeys } from "../../consts";
type ContentProps = {
guilds: GuildInfo[];
};
const SidebarContent = ({ guilds }: ContentProps) => {
const guildEntries = guilds.map((guild) => <GuildEntry guild={guild}></GuildEntry>);
return (
<>
<a href="/">
<Brand></Brand>
</a>
<Wave></Wave>
<aside class="menu">
<p class="menu-label">Servers</p>
<ul class="menu-list guildList">{guildEntries}</ul>
<div class="aside-footer">
<p class="menu-label">Options</p>
<ul class="menu-list">
<li>
<a class="show-modal" data-modal="dataManagerModal">
<span class="icon">
<i class="fas fa-exchange"></i>
</span>{" "}
Import/Export
</a>
<a class="show-modal" data-modal="chooseTimezoneModal">
<span class="icon">
<i class="fas fa-map-marked"></i>
</span>{" "}
Timezone
</a>
<a href="/login/discord/logout">
<span class="icon">
<i class="fas fa-sign-out"></i>
</span>{" "}
Log out
</a>
<a href="https://discord.jellywx.com" class="feedback">
<span class="icon">
<i class="fab fa-discord"></i>
</span>{" "}
Give feedback
</a>
</li>
</ul>
</div>
</aside>
</>
);
};
export const Sidebar = () => {
const { status, data } = useQuery({
queryKey: [QueryKeys.USER_GUILDS],
queryFn: fetchUserGuilds,
staleTime: Infinity,
});
let content = <SidebarContent guilds={[]}></SidebarContent>;
if (status === "success") {
content = <SidebarContent guilds={data}></SidebarContent>;
}
return (
<>
<DesktopSidebar>{content}</DesktopSidebar>
<MobileSidebar>{content}</MobileSidebar>
</>
);
};

View File

@ -0,0 +1,99 @@
import { DateTime } from "luxon";
import { useQuery } from "react-query";
import { QueryKeys } from "../../consts";
import { fetchUserInfo } from "../../api";
type DisplayProps = {
timezone: string;
};
const TimezoneDisplay = ({ timezone }: DisplayProps) => {
const now = DateTime.now().setZone(timezone);
const hour = now.hour;
const minute = now.minute;
return (
<>
<strong>
<span class="set-timezone">{timezone}</span>
</strong>{" "}
(
<span class="set-time">
{hour}:{minute}
</span>
)
</>
);
};
const TimezonePicker = () => {
const browserTimezone = DateTime.now().zone.name;
const { isLoading, isError, data } = useQuery({
queryKey: QueryKeys.USER_DATA,
queryFn: fetchUserInfo,
});
return (
<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{" "}
<a href="/help/timezone">
<span>
<i class="fa fa-question-circle"></i>
</span>
</a>
</label>
<button class="delete close-modal" aria-label="close"></button>
</header>
<section class="modal-card-body">
<p>
Your configured timezone is:{" "}
<TimezoneDisplay timezone={browserTimezone}></TimezoneDisplay>
<br></br>
<br></br>
Your browser timezone is:{" "}
<TimezoneDisplay timezone={browserTimezone}></TimezoneDisplay>
<br></br>
{!isError && (
<>
Your bot timezone is:{" "}
{isLoading ? (
<i className="fas fa-cog fa-spin"></i>
) : (
<TimezoneDisplay
timezone={data.timezone || "UTC"}
></TimezoneDisplay>
)}
</>
)}
</p>
<br></br>
<div class="has-text-centered">
<button class="button is-success close-modal" id="set-browser-timezone">
<span>Use Browser Timezone</span>{" "}
<span class="icon">
<i class="fab fa-firefox-browser"></i>
</span>
</button>
<button class="button is-link close-modal" id="set-bot-timezone">
<span>Use Bot Timezone</span>{" "}
<span class="icon">
<i class="fab fa-discord"></i>
</span>
</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>
);
};

View File

@ -0,0 +1,15 @@
export const Welcome = () => (
<section id="welcome">
<div class="has-text-centered">
<p class="title">Welcome!</p>
<p class="subtitle is-hidden-touch">Select an option from the side to get started</p>
<p class="subtitle is-hidden-desktop">
Press the{" "}
<span class="icon">
<i class="fal fa-bars"></i>
</span>{" "}
to get started
</p>
</div>
</section>
);

4
src/consts.ts Normal file
View File

@ -0,0 +1,4 @@
export enum QueryKeys {
USER_DATA = "userData",
USER_GUILDS = "userGuilds",
}

4
src/index.tsx Normal file
View File

@ -0,0 +1,4 @@
import { render } from "preact";
import { App } from "./components/App";
render(<App />, document.getElementById("app"));

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"allowJs": true,
"checkJs": true,
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"include": ["node_modules/vite/client.d.ts", "**/*"]
}

10
vite.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [preact()],
build: {
assetsDir: "static/assets",
},
});