Frontend Update

This commit is contained in:
CasVT
2024-02-23 12:37:10 +01:00
parent 683e2d5524
commit c6590f91dd
24 changed files with 1328 additions and 1090 deletions

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"configurations": [
{
"name": "Main File",
"type": "debugpy",
"request": "launch",
"python": "/bin/python3",
"program": "${workspaceFolder}/MIND.py"
}
]
}

View File

@@ -166,6 +166,11 @@ class Reminder:
).fetchone()
reminder = dict(reminder)
reminder["weekdays"] = [
int(n)
for n in reminder["weekdays"].split(",")
if n
] if reminder["weekdays"] else None
reminder['notification_services'] = self._get_notification_services()
return reminder
@@ -277,7 +282,7 @@ class Reminder:
if repeated_reminder:
next_time = _find_next_time(
data["time"],
RepeatQuantity(data["repeat_quantity"]),
RepeatQuantity(data["repeat_quantity"]) if data["repeat_quantity"] else None,
data["repeat_interval"],
weekdays
)
@@ -408,6 +413,12 @@ class Reminders:
(self.user_id,)
)
]
for r in reminders:
r["weekdays"] = [
int(n)
for n in r["weekdays"].split(",")
if n
] if r["weekdays"] else None
# Sort result
reminders.sort(key=sort_by.value[0], reverse=sort_by.value[1])

View File

@@ -23,7 +23,7 @@ class User:
def __init__(self, id: int) -> None:
result = get_db(dict).execute(
"SELECT username, admin FROM users WHERE id = ? LIMIT 1;",
"SELECT username, admin, salt FROM users WHERE id = ? LIMIT 1;",
(id,)
).fetchone()
if not result:
@@ -32,6 +32,7 @@ class User:
self.username: str = result['username']
self.user_id = id
self.admin: bool = result['admin'] == 1
self.salt: bytes = result['salt']
return
@property

View File

@@ -49,7 +49,7 @@ main {
overflow-y: auto;
padding: .5rem;
padding-top: calc(1rem + var(--nav-width));
padding-top: var(--nav-width);
}
#settings-form {
@@ -63,12 +63,13 @@ h2 {
width: 100%;
border-bottom: 1px solid var(--color-gray);
padding: 1rem 1rem 0rem 1rem;
padding: 1rem 1rem .25rem 1rem;
font-size: clamp(1rem, 10vw, 2rem);
}
.table-container {
.settings-table-container,
.user-table-container {
width: 100%;
overflow-x: auto;
@@ -108,6 +109,10 @@ h2 {
font-size: .9rem;
}
.settings-table td > p {
margin-top: .25rem;
}
.number-input {
width: fit-content;
display: flex;
@@ -126,8 +131,12 @@ h2 {
text-align: right;
}
.number-input > * {
padding: .5rem 1rem;
}
.number-input > p {
padding: .75rem .75rem .75rem 0rem;
padding-left: 0;
}
.settings-table select {
@@ -170,11 +179,10 @@ h2 {
#user-table {
min-width: 25rem;
border-spacing: 0px;
margin-bottom: 2rem;
}
#user-table th,
#user-table td {
#user-table :where(th, td) {
height: 2.65rem;
padding: .25rem .5rem;
text-align: left;
}
@@ -183,54 +191,53 @@ h2 {
border-top: 1px solid var(--color-gray);
}
#user-table th:first-child,
#user-table td:first-child {
#user-table :where(th, td):first-child {
width: 10rem;
padding-left: 2rem;
}
#user-table th:last-child,
#user-table td:last-child {
#user-table :where(td, td):nth-child(2) {
width: 15rem;
}
#user-table :where(th, td):last-child {
width: 5.75rem;
display: flex;
align-items: center;
gap: 1rem;
padding-right: 2rem;
}
#user-table td:first-child {
width: 100%;
#user-table button {
height: 1.25rem;
}
#user-table svg {
aspect-ratio: 1/1;
width: 1.25rem;
height: 1.25rem;
height: 100%;
width: auto;
}
#new-username-input,
#new-password-input {
width: 40%;
padding: .25rem;
}
#user-list form {
margin-top: .5rem;
width: 75%;
}
#user-list input[type="password"] {
#user-table input {
width: 100%;
padding: .25rem;
}
@media (max-width: 40rem) {
#settings-form,
.table-container {
#settings-form {
justify-content: flex-start;
}
h2 {
text-align: center;
padding-inline: 0;
}
.settings-table-container,
.user-table-container {
justify-content: left;
}
.settings-table tbody {
display: flex;
flex-direction: column;
@@ -239,6 +246,7 @@ h2 {
.settings-table tr {
display: inline-flex;
flex-direction: column;
padding-left: 1rem;
}
.settings-table td {
@@ -246,14 +254,11 @@ h2 {
}
.settings-table td:first-child {
width: 100%;
text-align: left;
}
.settings-table td:nth-child(2) {
min-width: 0;
}
#user-table {
width: 100%;
}
}

View File

@@ -28,30 +28,32 @@ img {
width: 100%;
}
button {
button,
label {
border: 0;
border-radius: 4px;
background-color: var(--color-dark);
color: var(--color-light);
}
button:hover {
button:hover,
label:hover {
cursor: pointer;
}
input:not([type="checkbox"]),
select,
textarea {
width: 100%;
textarea,
.as-button {
border: 2px solid var(--color-gray);
border-radius: 4px;
padding: .75rem;
padding: .6rem;
outline: 0;
box-shadow: var(--default-shadow);
background-color: var(--color-dark);
color: var(--color-light);
box-shadow: var(--default-shadow);
font-size: 1rem;
}
@@ -69,6 +71,21 @@ svg rect {
fill: var(--color-light);
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
background-color: var(--color-dark);
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 6px;
background-color: var(--color-gray);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--color-light-gray);
}
/* */
/* Utility classes */
/* */
@@ -76,17 +93,25 @@ svg rect {
display: none !important;
}
.error {
.error,
.error-container p {
color: var(--color-error) !important;
font-size: 1rem !important;
}
.error-container p {
display: block !important;
}
.error-icon path,
.error-icon rect {
.error-icon rect,
.error-container path,
.error-container rect {
fill: var(--color-error) !important;
}
.error-input {
.error-input,
.error-container input {
border: 2px solid var(--color-error) !important;
}
@@ -173,11 +198,11 @@ header img {
padding-block: var(--rem-clamp);
}
.nav-divider.show-nav > nav {
body:has(#nav-switch:checked) .nav-divider > nav {
left: var(--rem-clamp);
}
.nav-divider.show-nav > .window-container {
body:has(#nav-switch:checked) .nav-divider > .window-container {
margin-left: calc(var(--nav-width) + var(--rem-clamp));
}
@@ -237,72 +262,70 @@ nav > div > button svg {
}
/* */
/* Main window */
/* Window management */
/* */
.window-container {
margin-left: calc(4rem + var(--rem-clamp));
width: 100%;
display: flex;
overflow-y: auto;
overflow-x: hidden;
overflow: hidden;
transition: margin-left .3s ease-in-out;
}
.window-container > div {
.window-container > :where(#home, .extra-window-container) {
width: 100%;
flex: 0 0 auto;
transform: translateX(0);
transition: transform .5s ease-in-out;
translate: 0 0;
transition: translate .5s ease-in-out;
}
.window-container > div.show-window {
transform: translateX(-100%);
.window-container.inter-window-ani > :where(#home, .extra-window-container) {
transition: translate .5s ease-in-out,
transform .5s ease-in-out;
}
.window-container > div:not(#home) {
.extra-window-container {
--y-offset: 0%;
transform: translateY(var(--y-offset));
}
.extra-window-container > div {
height: 100%;
overflow-y: auto;
}
.window-container.show-window > :where(#home, .extra-window-container) {
translate: -100% 0;
}
.window-container.show-window > .extra-window-container {
transform: translateY(var(--y-offset));
}
/* */
/* Styling extra window */
/* */
.extra-window-container > div {
padding: var(--rem-clamp);
}
.window-container > div:not(#home) > h2 {
.extra-window-container > div > h2 {
text-align: center;
font-size: clamp(1.3rem, 5vw, 2rem);
margin-bottom: 2rem;
}
.window-container > div:not(#home) > h2:not(:first-of-type) {
.extra-window-container > div > h2:not(:first-of-type) {
margin-top: 1.5rem;
}
.window-container > div:not(#home) > p {
.extra-window-container > div > p {
text-align: center;
}
.tab-selector {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
padding-inline: .5rem;
padding-top: 1rem;
}
.tab-selector > button {
border: 2px solid var(--color-gray);
padding: .5rem 1rem;
transition: background-color .3s ease-in-out;
}
.tab-selector > button[data-selected="true"] {
background-color: var(--color-gray);
}
@media (max-width: 543px) {
.window-container {
margin-left: 0;
@@ -311,4 +334,4 @@ nav > div > button svg {
nav {
left: -100%;
}
}
}

View File

@@ -10,10 +10,9 @@
gap: 1rem;
}
.form-container > form input,
.form-container > form select,
.form-container > form textarea,
.form-container > form button {
.form-container > form :where(
input, select, textarea, button, label
) {
padding: 1rem;
}
@@ -27,14 +26,14 @@
gap: var(--gap);
}
.sub-inputs > input,
.sub-inputs > select,
.sub-inputs > button {
.sub-inputs > :where(
input, select, button, label
) {
width: calc(50% - (var(--gap) / 2));
}
.form-container > form > button,
.sub-inputs > button {
.sub-inputs > :where(button, label) {
display: flex;
justify-content: center;
align-items: center;
@@ -55,8 +54,17 @@
opacity: 0;
}
.color-list {
#color-button {
--color: var(--color-dark);
background-color: var(--color);
}
#info-form:has(#color-toggle:checked) .color-list {
display: flex;
}
.color-list {
display: none;
justify-content: center;
align-items: center;
gap: 1rem;
@@ -68,25 +76,26 @@
box-shadow: var(--default-shadow);
}
.color-list > button {
height: 1.5rem;
width: 1.5rem;
.color-list > label {
padding: 1rem;
border: 1px solid transparent;
background-color: var(--color);
}
.color-list > button[data-selected='true'] {
.color-list > label:has(input:checked) {
border-color: var(--color-white);
}
.notification-service-list {
#info-form:has(#notification-service-selection-toggle:checked) .notification-service-selection {
display: flex;
}
.notification-service-selection {
width: 100%;
max-height: 10rem;
overflow-y: auto;
display: flex;
display: none;
flex-direction: column;
border: 2px solid var(--color-gray);
@@ -95,19 +104,20 @@
box-shadow: var(--default-shadow);
}
.notification-service-list > div {
.notification-service-selection > div {
display: flex;
gap: 1rem;
padding: .5rem .75rem;
}
.notification-service-list > div:not(:first-child) {
.notification-service-selection > div:not(:first-child) {
border-top: 1px solid var(--color-gray);
}
.form-container > form .repeat-options > button {
width: calc((100% / 3) - (var(--gap) / 1.5));
min-width: min-content;
padding: 1rem 0rem;
}
.repeat-bar {
@@ -214,6 +224,7 @@ div.options > button {
.sub-inputs > input,
.sub-inputs > select,
.sub-inputs > button,
.sub-inputs > label,
.form-container > form .repeat-options > button {
width: 100%;
}
@@ -258,8 +269,8 @@ div.options > button {
}
#info.show-add-static-reminder #template-selection,
#info.show-add-static-reminder #color-toggle,
#info.show-add-static-reminder #toggle-notification-service-list {
#info.show-add-static-reminder #color-button,
#info.show-add-static-reminder #notification-service-selection-button {
width: 100%;
}
@@ -279,8 +290,8 @@ div.options > button {
margin-top: -1rem;
}
#info.show-add-template #color-toggle,
#info.show-add-template #toggle-notification-service-list {
#info.show-add-template #color-button,
#info.show-add-template #notification-service-selection-button {
width: 100%;
}
@@ -292,7 +303,7 @@ div.options > button {
display: none;
}
#info.show-edit-reminder #color-toggle {
#info.show-edit-reminder #color-button {
width: 100%;
}
@@ -310,8 +321,8 @@ div.options > button {
margin-top: -1rem;
}
#info.show-edit-static-reminder #color-toggle,
#info.show-edit-static-reminder #toggle-notification-service-list {
#info.show-edit-static-reminder #color-button,
#info.show-edit-static-reminder #notification-service-selection-button {
width: 100%;
}
@@ -330,7 +341,7 @@ div.options > button {
margin-top: -1rem;
}
#info.show-edit-template #color-toggle,
#info.show-edit-template #toggle-notification-service-list {
#info.show-edit-template #color-button,
#info.show-edit-template #notification-service-selection-button {
width: 100%;
}

View File

@@ -1,8 +1,10 @@
/* */
/* SEARCH BAR */
/* */
.search-container {
max-width: 40rem;
margin: auto;
padding-block: clamp(1rem, 4vw, 2rem);
}
@@ -12,24 +14,28 @@
.search-bar {
display: flex;
border: 2px solid var(--color-gray);
border-radius: 4px;
box-shadow: var(--default-shadow);
}
.search-bar button {
width: 3.5rem;
padding: .8rem;
.search-bar :where(button, label) {
width: clamp(2rem, 7.5vw, 3rem);
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
.search-bar button svg {
width: 1rem;
.search-bar :where(button, label) svg {
height: 1rem;
}
.search-bar input {
width: 100%;
border: 0;
padding-block: 1rem;
box-shadow: none;
@@ -37,7 +43,7 @@
#clear-button {
opacity: 0;
transition: opacity .1s linear;
}
@@ -46,26 +52,44 @@
}
#sort-input {
width: min-content;
width: clamp(6rem, 25vw, 12rem);
border: 0;
box-shadow: none;
}
.window-container:has(
:where(#static-tab-selector, #template-tab-selector):checked
) #sort-input > option:where(
[value="time"], [value="time_reversed"]
) {
display: none;
}
/* */
/* REMINDER LIST */
#reminder-tab,
#static-reminder-tab,
#template-tab {
/* */
.tab-container > div {
--gap: 1rem;
--entry-width: 13rem;
max-width: 43rem;
margin-inline: auto;
display: flex;
display: none;
justify-content: left;
gap: var(--gap);
flex-wrap: wrap;
padding: 1rem;
transition: max-width .75s ease-in-out;
}
body:has(#wide-toggle:checked) .tab-container > div {
max-width: 85rem;
}
#home {
overflow-y: auto;
}
.entry.add-entry {
@@ -122,10 +146,61 @@ button.entry.fit {
font-weight: 500;
}
/* */
/* Tab selector */
/* */
.tab-selector {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
padding-inline: 1rem;
padding-top: 1rem;
}
.tab-selector > input {
display: none;
}
.tab-selector > label {
min-width: 9.55rem;
padding: .5rem 1rem;
border-radius: 4px;
border: 2px solid var(--color-gray);
background-color: var(--color-dark);
color: var(--color-light);
text-align: center;
transition: background-color .3s ease-in-out;
}
.tab-selector > input:checked + label {
background-color: var(--color-gray);
}
.window-container:has(#reminder-tab-selector:checked) #reminder-tab,
.window-container:has(#static-tab-selector:checked) #static-reminder-tab,
.window-container:has(#template-tab-selector:checked) #template-tab {
display: flex;
}
@media (max-width: 543px) {
header > div {
transform: translateX(0);
}
.tab-selector > label {
flex: 1 0 25%;
}
#wide-button {
display: none;
}
.entry {
flex-grow: 1;

View File

@@ -4,7 +4,7 @@ main {
overflow-y: hidden;
}
main.show-create > .form-container {
main:has(#form-switch:checked) .form-container {
transform: translateY(-100%);
}
@@ -16,7 +16,7 @@ main.show-create > .form-container {
align-items: center;
padding: 1rem;
transition: transform .25s ease-in-out;
}
@@ -40,8 +40,24 @@ form h2 {
font-size: clamp(1.2rem, 7vw, 2rem);
}
#username-error-container,
#password-error-container {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: .5rem;
}
form input {
max-width: 20rem;
width: min(100%, 20rem);
}
#username-error-container p,
#password-error-container p {
width: min(100%, 19rem);
}
.switch-button {
@@ -50,6 +66,11 @@ form input {
color: var(--color-light);
text-decoration: underline;
text-align: center;
}
.switch-button:hover {
cursor: pointer;
}
button[type="submit"] {
@@ -57,13 +78,3 @@ button[type="submit"] {
font-size: 1.1rem;
}
p.error {
width: 100%;
max-width: 20rem;
margin-top: -.5rem;
padding-inline: .5rem;
text-align: left;
}

View File

@@ -8,19 +8,30 @@
}
#add-service-button {
padding: .5rem 8rem;
width: min(100%, 17rem);
height: 2rem;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
background-color: var(--color-gray);
}
#add-service-button > svg {
height: 1rem;
width: 1rem;
transition: transform .125s linear;
}
.table-container:has(#service-list-toggle:checked) #add-service-button > svg {
transform: rotate(45deg);
}
.overflow-container {
margin-inline: auto;
width: 100%;
max-width: 50rem;
width: min(100%, 50rem);
overflow-x: auto;
}
@@ -29,6 +40,10 @@
border-spacing: 0px;
}
.overflow-container > table:not(:has(tbody > tr)) {
display: none;
}
.overflow-container > table th,
.overflow-container > table td {
text-align: left;
@@ -38,7 +53,7 @@
padding: .5rem;
}
#services-list > tr > td {
.overflow-container td {
border-top: 1px solid var(--color-gray);
padding: .25rem;
}
@@ -46,7 +61,7 @@
.title-column {
min-width: 9.5rem;
width: 25%;
padding-left: 1.5rem;
padding-right: 1rem;
}
@@ -56,17 +71,18 @@
width: 65%;
}
#services-list input {
border-radius: 2px;
.overflow-container table input {
width: 100%;
border-radius: 4px;
padding: .25rem;
box-shadow: none;
}
#services-list input:read-only {
.overflow-container input:read-only {
border-color: transparent;
}
#services-list > tr > td.action-column {
.overflow-container .action-column {
min-width: 4rem;
width: 20%;
@@ -77,18 +93,24 @@
padding-right: 1.5rem;
}
.action-column svg {
width: 1rem;
height: 1rem;
.action-column > button {
display: flex;
justify-content: center;
align-items: center;
}
#services-list > tr:not(.edit) > td.action-column > button[data-type="edit"],
#services-list > tr.edit > td.action-column > button[data-type="save"] {
display: inline-block;
.overflow-container .action-column svg {
width: 1.25rem;
height: 1.25rem;
}
#services-list > tr.edit > td.action-column > button[data-type="edit"],
#services-list > tr:not(.edit):not(#add-row) > td.action-column > button[data-type="save"] {
tr:has(input:not(:read-only)) button[data-type="save"],
tr:has(input:read-only) button[data-type="edit"] {
display: flex;
}
tr:has(input:not(:read-only)) button[data-type="edit"],
tr:has(input:read-only) button[data-type="save"] {
display: none;
}
@@ -99,14 +121,22 @@
display: none;
}
.overflow-container.show-add #add-service-container {
.overflow-container:has(#service-list-toggle:checked) table {
display: none;
}
.overflow-container:has(#service-list-toggle:checked) #add-service-container {
display: block;
}
.overflow-container.show-add > table {
.overflow-container:has(#add-service-toggle:checked) #service-list {
display: none;
}
.overflow-container:has(#add-service-toggle:checked) #add-service-window {
display: flex;
}
#service-list {
display: flex;
gap: 1rem;
@@ -130,14 +160,6 @@
font-size: 1.1rem;
}
#add-service-container.show-add-window #add-service-window {
display: flex;
}
#add-service-container.show-add-window #service-list {
display: none;
}
/* */
/* Add service form */
/* */
@@ -205,7 +227,7 @@
min-height: 5rem;
max-height: 15rem;
overflow-y: auto;
align-items: center;
background-color: var(--color-dark);
@@ -228,19 +250,23 @@
}
.add-row {
height: 2rem;
width: 80%;
width: min(100%, 21rem);
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 1rem;
}
.add-row input {
flex-grow: 1;
height: 2rem;
min-width: 0rem;
font-size: .8rem;
}
.add-row button {
height: 2rem;
padding: .35rem .75rem;
background-color: var(--color-gray);
border-radius: 4px;
@@ -265,3 +291,9 @@
height: inherit;
fill: var(--color-dark);
}
@media (max-width: 543px) {
#service-list button {
flex-grow: 1;
}
}

View File

@@ -14,6 +14,7 @@ h2 {
p {
font-size: clamp(1rem, 4.2vw, 1.75rem);
text-align: center;
}
a {

View File

@@ -1,45 +1,39 @@
.settings-container {
max-width: 50rem;
#settings-table {
margin-inline: auto;
text-align: center;
width: min(100%, 50rem);
}
.settings-container > h3 {
margin-bottom: 1.25rem;
padding-bottom: .5rem;
border-bottom: 2px solid var(--color-gray);
font-size: 1.4rem;
#settings-table td {
padding: .5rem;
width: 50%;
}
.settings-container > h3:not(:first-of-type) {
margin-top: 2.5rem;
#settings-table tr > td:first-child {
text-align: right;
font-weight: 500;
}
.settings-container button {
#settings-table button {
padding: .5rem 2rem;
background-color: var(--color-gray);
box-shadow: var(--default-shadow);
}
.settings-container input,
.settings-container textarea,
.settings-container select {
max-width: 20rem;
#settings-table tr:has(#default-service-input) {
display: none;
}
#settings-table tr:has(#default-service-input option) {
display: table-row;
}
#change-password-form {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
}
#delete-account-button {
#settings-table #delete-account-button {
background-color: var(--color-error);
}
@@ -67,4 +61,4 @@
text-decoration: none;
font-size: 1.2rem;
}
}

View File

@@ -1,7 +1,12 @@
const setting_inputs = {
'allow_new_accounts': document.querySelector('#allow-new-accounts-input'),
'login_time': document.querySelector('#login-time-input'),
'login_time_reset': document.querySelector('#login-time-reset-input')
allow_new_accounts: document.querySelector('#allow-new-accounts-input'),
login_time: document.querySelector('#login-time-input'),
login_time_reset: document.querySelector('#login-time-reset-input')
};
const user_inputs = {
username: document.querySelector('#new-username-input'),
password: document.querySelector('#new-password-input')
};
function checkLogin() {
@@ -58,8 +63,8 @@ function toggleAddUser() {
const el = document.querySelector('#add-user-row');
if (el.classList.contains('hidden')) {
// Show row
document.querySelector('#new-username-input').value = '';
document.querySelector('#new-password-input').value = '';
user_inputs.username.value = '';
user_inputs.password.value = '';
el.classList.remove('hidden');
} else {
// Hide row
@@ -68,9 +73,11 @@ function toggleAddUser() {
};
function addUser() {
user_inputs.username.classList.remove('error-input');
user_inputs.username.title = '';
const data = {
'username': document.querySelector('#new-username-input').value,
'password': document.querySelector('#new-password-input').value
'username': user_inputs.username.value,
'password': user_inputs.password.value
};
fetch(`${url_prefix}/api/admin/users?api_key=${api_key}`, {
'method': 'POST',
@@ -85,12 +92,23 @@ function addUser() {
loadUsers();
})
.catch(e => {
console.log(e);
if (e === "UsernameTaken") {
user_inputs.username.classList.add('error-input');
user_inputs.username.title = 'Username already taken';
} else if (e === "UsernameInvalid") {
user_inputs.username.classList.add('error-input');
user_inputs.username.title = 'Username contains invalid characters';
} else
console.log(e);
});
};
function editUser(id) {
const new_password = document.querySelector(`#user-table tr[data-id="${id}"] input`).value;
const new_password = document.querySelector(
`#user-table tr[data-id="${id}"] input`
).value;
fetch(`${url_prefix}/api/admin/users/${id}?api_key=${api_key}`, {
'method': 'PUT',
'headers': {'Content-Type': 'application/json'},
@@ -103,7 +121,7 @@ function deleteUser(id) {
document.querySelector(`#user-table tr[data-id="${id}"]`).remove();
fetch(`${url_prefix}/api/admin/users/${id}?api_key=${api_key}`, {
'method': 'DELETE'
})
});
};
function loadUsers() {
@@ -117,9 +135,10 @@ function loadUsers() {
entry.dataset.id = user.id;
const username = document.createElement('td');
const username_text = document.createElement('p');
username_text.innerText = user.username;
username.appendChild(username_text);
username.innerText = user.username;
entry.appendChild(username);
const password = document.createElement('td');
const new_password_form = document.createElement('form');
new_password_form.classList.add('hidden');
new_password_form.action = `javascript:editUser(${user.id})`;
@@ -127,23 +146,29 @@ function loadUsers() {
new_password.type = 'password';
new_password.placeholder = 'New password';
new_password_form.appendChild(new_password);
username.appendChild(new_password_form);
entry.appendChild(username);
password.appendChild(new_password_form);
entry.appendChild(password);
const actions = document.createElement('td');
entry.appendChild(actions);
const edit_user = document.createElement('button');
edit_user.onclick = e => e.currentTarget.parentNode.previousSibling.querySelector('form').classList.toggle('hidden');
edit_user.innerHTML = icons.edit;
edit_user.onclick = e => e
.currentTarget
.parentNode
.previousSibling
.querySelector('form')
.classList
.toggle('hidden');
edit_user.innerHTML = Icons.edit;
actions.appendChild(edit_user);
if (user.username !== 'admin') {
const delete_user = document.createElement('button');
delete_user.onclick = e => deleteUser(user.id);
delete_user.innerHTML = icons.delete;
delete_user.innerHTML = Icons.delete;
actions.appendChild(delete_user);
}
};
table.appendChild(entry);
});
@@ -156,9 +181,9 @@ checkLogin();
loadSettings();
loadUsers();
document.querySelector('#logout-button').onclick = (e) => logout();
document.querySelector('#logout-button').onclick = e => logout();
document.querySelector('#settings-form').action = 'javascript:submitSettings();';
document.querySelector('#add-user-button').onclick = e => toggleAddUser();
document.querySelector('#add-user-form').action = 'javascript:addUser()';
document.querySelector('#download-db-button').onclick = e =>
window.location.href = `${url_prefix}/api/admin/database?api_key=${api_key}`
window.location.href = `${url_prefix}/api/admin/database?api_key=${api_key}`;

View File

@@ -1,46 +1,49 @@
const types = {
// The duration of the animation set for the window translation
const window_ani_ms = 500;
const Types = {
'reminder': document.getElementById('reminder-tab'),
'static_reminder': document.getElementById('static-reminder-tab'),
'template': document.getElementById('template-tab')
};
const icons = {
const Icons = {
'save': '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><path d="M12,10a4,4,0,1,0,4,4A4,4,0,0,0,12,10Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,16Z"></path><path d="M22.536,4.122,19.878,1.464A4.966,4.966,0,0,0,16.343,0H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H19a5.006,5.006,0,0,0,5-5V7.657A4.966,4.966,0,0,0,22.536,4.122ZM17,2.08V3a3,3,0,0,1-3,3H10A3,3,0,0,1,7,3V2h9.343A2.953,2.953,0,0,1,17,2.08ZM22,19a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2V3a5.006,5.006,0,0,0,5,5h4a4.991,4.991,0,0,0,4.962-4.624l2.16,2.16A3.02,3.02,0,0,1,22,7.657Z"></path></g></svg>',
'edit': '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22.94,1.06a3.626,3.626,0,0,0-5.124,0L0,18.876V24H5.124L22.94,6.184A3.627,3.627,0,0,0,22.94,1.06ZM4.3,22H2V19.7L15.31,6.4l2.3,2.3ZM21.526,4.77,19.019,7.277l-2.295-2.3L19.23,2.474a1.624,1.624,0,0,1,2.3,2.3Z"></path></g></g></svg>',
'delete': '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g id="_01_align_center" data-name="01 align center"><path d="M22,4H17V2a2,2,0,0,0-2-2H9A2,2,0,0,0,7,2V4H2V6H4V21a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V6h2ZM9,2h6V4H9Zm9,19a1,1,0,0,1-1,1H7a1,1,0,0,1-1-1V6H18Z"></path><rect x="9" y="10" width="2" height="8"></rect><rect x="13" y="10" width="2" height="8"></rect></g></g></svg>',
'add': '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><g><g><path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path></g></g></svg>'
};
const info_classes = [
const InfoClasses = [
'show-add-reminder', 'show-add-template', 'show-add-static-reminder',
'show-edit-reminder', 'show-edit-template', 'show-edit-static-reminder'
];
function toggleNav() {
document.querySelector('.nav-divider').classList.toggle('show-nav');
};
function showWindow(id) {
document.querySelectorAll('.window-container > div').forEach(e => {
if (e.id === id || e.id === 'home') {
e.classList.remove('hidden');
setTimeout(() => e.classList.add('show-window'), 0);
} else {
e.classList.add('hidden');
e.classList.remove('show-window');
};
});
};
function hideWindow() {
document.querySelectorAll('.show-window').forEach(e => {
e.classList.remove('show-window');
});
setTimeout(() => {
document.querySelectorAll('.window-container > div:not(#home)').forEach(
e => e.classList.add('hidden')
const window_container = document.querySelector('.window-container');
if (id === "home") {
window_container.classList.remove(
'show-window',
'inter-window-ani'
);
}, 500);
} else {
const extra_window_container =
document.querySelector('.extra-window-container');
const offset = [
...extra_window_container.children
].indexOf(document.getElementById(id)) * -100;
extra_window_container.style.setProperty(
'--y-offset',
`${offset}%`
)
window_container.classList.add('show-window');
setTimeout(
() => window_container.classList.add('inter-window-ani'),
window_ani_ms
);
};
};
function logout() {
@@ -59,7 +62,10 @@ function logout() {
const default_values = {
'api_key': null,
'locale': 'en-GB',
'default_service': null
'default_service': null,
'sorting_reminders': 'time',
'sorting_static': 'title',
'sorting_templates': 'title'
};
function setupLocalStorage() {

View File

@@ -1,73 +1,80 @@
const sorting_options = {};
sorting_options[types.reminder.id] = [
['time', 'Time'],
['time_reversed', 'Time Reversed'],
['title', 'Title'],
['title_reversed', 'Title Reversed'],
['date_added', 'Date Added'],
['date_added_reversed', 'Date Added Reversed']
];
sorting_options[types.static_reminder.id] = [
['title', 'Title'],
['title_reversed', 'Title Reversed'],
['date_added', 'Date Added'],
['date_added_reversed', 'Date Added Reversed']
];
sorting_options[types.template.id] = [
['title', 'Title'],
['title_reversed', 'Title Reversed'],
['date_added', 'Date Added'],
['date_added_reversed', 'Date Added Reversed']
];
function showTab(button) {
// Apply styling to selected button
document.querySelectorAll('.tab-selector > button').forEach(
b => b.dataset.selected = b === button ? 'true' : 'false'
);
// Show desired tab and hide all others
document.querySelectorAll('#home > div:not(.tab-selector):not(.search-container)').forEach(
e => e.classList.add('hidden')
);
document.getElementById(button.dataset.target).classList.remove('hidden');
fillSortOptions();
document.querySelector('#search-input').value = '';
const NavButtons = {
home: document.querySelector('#home-button'),
notification_services: document.querySelector('#notification-services-button'),
settings: document.querySelector('#settings-button'),
log_out: document.querySelector('#logout-button')
};
//
// Filling library
//
function getWeekDays(locale)
{
let baseDate = new Date(Date.UTC(2017, 0, 2)); // just a Monday
let weekDays = [];
for(i = 0; i < 7; i++)
const LibEls = {
tab_selector: document.querySelector('.tab-selector'),
tab_container: document.querySelector('.tab-container'),
search_bar: {
form: document.querySelector('#search-form'),
input: document.querySelector('#search-input'),
clear: document.querySelector('#clear-button'),
sort: document.querySelector('#sort-input'),
wide: document.querySelector('#wide-button')
}
};
//
// Helpers
//
function getSorting(type, key=false) {
let sorting_key;
if (type === Types.reminder)
sorting_key = 'sorting_reminders';
else if (type === Types.static_reminder)
sorting_key = 'sorting_static';
else if (type === Types.template)
sorting_key = 'sorting_templates';
if (key)
return sorting_key;
else
return getLocalStorage(sorting_key)[sorting_key];
};
function getWeekDays(locale) {
const baseDate = new Date(Date.UTC(2017, 0, 2)); // just a Monday
const weekDays = [];
for (i = 0; i < 7; i++)
{
weekDays.push(baseDate.toLocaleDateString(locale, { weekday: 'short' }));
baseDate.setDate(baseDate.getDate() + 1);
}
return weekDays;
};
const week_days = getWeekDays(getLocalStorage('locale')['locale']);
function getActiveTab() {
for (let t of Object.values(Types)) {
if (getComputedStyle(t).display === 'flex')
return t
};
return null;
};
//
// Filling library
//
function fillTable(table, results) {
table.querySelectorAll('button.entry:not(.add-entry)').forEach(e => e.remove());
table.querySelectorAll('button.entry:not(.add-entry)').forEach(
e => e.remove()
);
results.forEach(r => {
const entry = document.createElement('button');
entry.classList.add('entry');
entry.dataset.id = r.id;
entry.addEventListener('click', e => showEdit(r.id, table));
entry.onclick = e => showEdit(r.id, table);
if (r.color !== null)
entry.style.setProperty('--color', r.color);
const title = document.createElement('h2');
title.innerText = r.title;
entry.appendChild(title);
if (table === types.reminder) {
if (table === Types.reminder) {
const time = document.createElement('p');
let offset = new Date(r.time * 1000).getTimezoneOffset() * -60;
let d = new Date((r.time + offset) * 1000);
@@ -83,140 +90,43 @@ function fillTable(table, results) {
formatted_date += interval_text;
} else if (r.weekdays !== null)
formatted_date += ` (each ${r.weekdays.split(',').map(d => week_days[parseInt(d)]).join(', ')})`;
formatted_date += ` (each ${r.weekdays.map(d => week_days[d]).join(', ')})`;
time.innerText = formatted_date;
entry.appendChild(time);
};
table.appendChild(entry);
if (title.clientHeight < title.scrollHeight)
entry.classList.add('expand');
});
table.querySelectorAll('button.entry:not(.add-entry)').forEach(r => r.classList.add('fit'));
evaluateSizing();
};
function fillLibrary(url, type) {
fetch(url)
.then(response => {
if (!response.ok) return Promise.reject(response.status);
return response.json();
})
.then(json => fillTable(type, json.result))
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
else
console.log(e);
});
};
function fillLibrary(type=null) {
let tab_type = type || getActiveTab();
function fillReminders() {
const sorting = document.querySelector('#sort-input').value;
fillLibrary(`/api/reminders?api_key=${api_key}&sort_by=${sorting}`, types.reminder);
};
function fillStaticReminders(assume_sorting=false) {
let sorting;
if (assume_sorting)
sorting = sorting_options[types.static_reminder.id][0][0];
else
sorting = document.querySelector('#sort-input').value;
fillLibrary(`/api/staticreminders?api_key=${api_key}&sort_by=${sorting}`, types.static_reminder);
}
function fillTemplates(assume_sorting=false) {
let sorting;
if (assume_sorting)
sorting = sorting_options[types.template.id][0][0];
else
sorting = document.querySelector('#sort-input').value;
fillLibrary(`/api/templates?api_key=${api_key}&sort_by=${sorting}`, types.template);
};
//
// Library search
//
function searchLibrary() {
const query = document.querySelector('#search-input').value,
tab = document.getElementById(
document.querySelector('.tab-selector > button[data-selected="true"]').dataset.target
)
const sorting = document.querySelector('#sort-input').value;
let url;
if (tab === types.reminder)
url = `${url_prefix}/api/reminders/search?api_key=${api_key}&query=${query}&sort_by=${sorting}`;
else if (tab === types.static_reminder)
url = `${url_prefix}/api/staticreminders/search?api_key=${api_key}&query=${query}&sort_by=${sorting}`;
else if (tab === types.template)
url = `${url_prefix}/api/templates/search?api_key=${api_key}&query=${query}&sort_by=${sorting}`;
else return;
fillLibrary(url, tab);
};
function clearSearchLibrary() {
document.querySelector('#search-input').value = '';
const tab = document.getElementById(
document.querySelector('.tab-selector > button[data-selected="true"]').dataset.target
)
if (tab === types.reminder)
fillReminders();
else if (tab === types.static_reminder)
fillStaticReminders();
else if (tab === types.template)
fillTemplates();
else return;
};
//
// Library sort
//
function fillSortOptions() {
const tab = document.getElementById(
document.querySelector('.tab-selector > button[data-selected="true"]').dataset.target
)
const sort_options = sorting_options[tab.id];
const select = document.getElementById('sort-input');
select.innerHTML = '';
sort_options.forEach(o => {
const entry = document.createElement('option');
entry.value = o[0]
entry.innerText = o[1]
select.appendChild(entry);
});
select.querySelector(':first-child').setAttribute('selected', '');
};
function applySorting() {
const query = document.querySelector('#search-input').value;
if (query !== '') {
searchLibrary();
if (tab_type === Types.reminder)
url = `${url_prefix}/api/reminders`;
else if (tab_type === Types.static_reminder)
url = `${url_prefix}/api/staticreminders`;
else if (tab_type === Types.template)
url = `${url_prefix}/api/templates`;
else
return;
};
const sorting = document.getElementById('sort-input').value,
tab = document.getElementById(
document.querySelector('.tab-selector > button[data-selected="true"]').dataset.target
)
let url;
if (tab === types.reminder)
url = `${url_prefix}/api/reminders?api_key=${api_key}&sort_by=${sorting}`;
else if (tab === types.static_reminder)
url = `${url_prefix}/api/staticreminders?api_key=${api_key}&sort_by=${sorting}`;
else if (tab === types.template)
url = `${url_prefix}/api/templates?api_key=${api_key}&sort_by=${sorting}`;
else return;
const sorting = getSorting(tab_type);
const query = LibEls.search_bar.input.value;
if (query)
url = `${url}/search?api_key=${api_key}&sort_by=${sorting}&query=${query}`;
else
url = `${url}?api_key=${api_key}&sort_by=${sorting}`;
fetch(url)
.then(response => {
if (!response.ok) return Promise.reject(response.status);
return response.json();
})
.then(json => fillTable(tab, json.result))
.then(json => fillTable(tab_type, json.result))
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
@@ -225,25 +135,50 @@ function applySorting() {
});
};
function saveSorting() {
const type = getActiveTab();
const sorting_key = getSorting(type, key=true);
const value = LibEls.search_bar.sort.value;
setLocalStorage({[sorting_key]: value});
};
function evaluateSizing() {
const tab = getActiveTab();
const entries = [...tab.querySelectorAll('button:not(.add-entry)')];
entries.forEach(e => e.classList.remove('fit'));
entries.forEach(e => {
const title = e.querySelector('h2');
if (title.clientHeight < title.scrollHeight)
e.classList.add('expand');
});
entries.forEach(e => e.classList.add('fit'));
};
// code run on load
document.querySelectorAll('.tab-selector > button').forEach(b => {
b.addEventListener('click', e => showTab(b));
Object.values(Types).forEach(t => fillLibrary(t));
setInterval(() => fillLibrary(Types.reminder), 60000);
const week_days = getWeekDays(getLocalStorage('locale')['locale']);
NavButtons.home.onclick = e => showWindow("home");
NavButtons.notification_services.onclick = e => showWindow("notification");
NavButtons.settings.onclick = e => showWindow("settings");
NavButtons.log_out.onclick = e => logout();
LibEls.search_bar.form.action = 'javascript:fillLibrary();'
LibEls.search_bar.sort.value = getSorting(getActiveTab());
LibEls.search_bar.sort.onchange = e => {
saveSorting();
fillLibrary();
};
LibEls.search_bar.clear.onclick = e => {
LibEls.search_bar.input.value = '';
fillLibrary();
};
LibEls.tab_selector.querySelectorAll('input').forEach(r => r.onchange = e => {
evaluateSizing();
LibEls.search_bar.input.value = '';
LibEls.search_bar.sort.value = getSorting(getActiveTab());
});
document.getElementById('toggle-nav').addEventListener('click', e => toggleNav());
document.getElementById('home-button').addEventListener('click', e => hideWindow());
document.getElementById('notification-services-button').addEventListener('click', e => showWindow('notification'));
document.getElementById('settings-button').addEventListener('click', e => showWindow('settings'));
document.getElementById('logout-button').addEventListener('click', e => logout());
fillSortOptions();
fillReminders();
fillStaticReminders(assume_sorting=true);
fillTemplates(assume_sorting=true);
setInterval(fillReminders, 60000);
document.querySelector('#search-form').setAttribute('action', 'javascript:searchLibrary();');
document.querySelector('#clear-button').addEventListener('click', e => clearSearchLibrary());
document.querySelector('#sort-input').addEventListener('change', e => applySorting());

View File

@@ -1,37 +1,36 @@
const login_inputs = {
'username': document.querySelector('#login-form > input[type="text"]'),
'password': document.querySelector('#login-form > input[type="password"]')
};
const login_errors = {
'username': document.getElementById('username-error'),
'password': document.getElementById('password-error')
};
const create_inputs = {
'username': document.querySelector('#create-form > input[type="text"]'),
'password': document.querySelector('#create-form > input[type="password"]')
}
const create_errors = {
'username_invalid': document.getElementById('new-username-error'),
'username_taken': document.getElementById('taken-username-error'),
};
function toggleWindow() {
document.querySelector('main').classList.toggle('show-create');
const forms = {
'login': {
'form': document.querySelector('#login-form'),
'inputs': {
'username': document.querySelector('#login-form input[type="text"]'),
'password': document.querySelector('#login-form input[type="password"]')
},
'errors': {
'username': document.querySelector("#username-error-container"),
'password': document.querySelector("#password-error-container")
}
},
'create': {
'form': document.querySelector('#create-form'),
'inputs': {
'username': document.querySelector('#create-form input[type="text"]'),
'password': document.querySelector('#create-form input[type="password"]')
},
'errors': {
'username_invalid': document.querySelector('#new-username-error'),
'username_taken': document.querySelector('#taken-username-error')
}
}
};
function login(data=null) {
login_inputs.username.classList.remove('error-input');
login_errors.username.classList.add('hidden');
login_inputs.password.classList.remove('error-input');
login_errors.password.classList.add('hidden');
forms.login.errors.username.classList.remove('error-container');
forms.login.errors.password.classList.remove('error-container');
if (data === null)
data = {
'username': login_inputs.username.value,
'password': login_inputs.password.value
'username': forms.login.inputs.username.value,
'password': forms.login.inputs.password.value
};
fetch(`${url_prefix}/api/auth/login`, {
@@ -53,26 +52,23 @@ function login(data=null) {
window.location.href = `${url_prefix}/reminders`;
})
.catch(e => {
if (e === 401) {
login_inputs.password.classList.add('error-input');
login_errors.password.classList.remove('hidden');
} else if (e === 404) {
login_inputs.username.classList.add('error-input');
login_errors.username.classList.remove('hidden');
} else {
if (e === 404)
forms.login.errors.username.classList.add('error-container');
else if (e === 401)
forms.login.errors.password.classList.add('error-container');
else
console.log(e);
};
});
};
function create() {
create_inputs.username.classList.remove('error-input');
create_errors.username_invalid.classList.add('hidden');
create_errors.username_taken.classList.add('hidden');
forms.create.inputs.username.classList.remove('error-input');
forms.create.errors.username_invalid.classList.add('hidden');
forms.create.errors.username_taken.classList.add('hidden');
const data = {
'username': create_inputs.username.value,
'password': create_inputs.password.value
'username': forms.create.inputs.username.value,
'password': forms.create.inputs.password.value
};
fetch(`${url_prefix}/api/user/add`, {
'method': 'POST',
@@ -86,11 +82,11 @@ function create() {
})
.catch(e => {
if (e === 'UsernameInvalid') {
create_inputs.username.classList.add('error-input');
create_errors.username_invalid.classList.remove('hidden');
forms.create.inputs.username.classList.add('error-input');
forms.create.errors.username_invalid.classList.remove('hidden');
} else if (e === 'UsernameTaken') {
create_inputs.username.classList.add('error-input');
create_errors.username_taken.classList.remove('hidden');
forms.create.inputs.username.classList.add('error-input');
forms.create.errors.username_taken.classList.remove('hidden');
} else {
console.log(e);
};
@@ -98,7 +94,7 @@ function create() {
};
function checkLogin() {
fetch(`${url_prefix}/api/auth/status?api_key=${JSON.parse(localStorage.getItem('MIND')).api_key}`)
fetch(`${url_prefix}/api/auth/status?api_key=${api_key}`)
.then(response => {
if (!response.ok) return Promise.reject(response.status);
return response.json();
@@ -129,13 +125,15 @@ function checkAllowNewAccounts() {
// code run on load
if (localStorage.getItem('MIND') === null)
localStorage.setItem('MIND', JSON.stringify({'api_key': null, 'locale': 'en-GB', 'default_service': null}))
localStorage.setItem('MIND', JSON.stringify(
{'api_key': null, 'locale': 'en-GB', 'default_service': null}
))
const url_prefix = document.getElementById('url_prefix').dataset.value;
const api_key = JSON.parse(localStorage.getItem('MIND')).api_key;
checkLogin();
checkAllowNewAccounts();
document.getElementById('login-form').setAttribute('action', 'javascript:login();');
document.getElementById('create-form').setAttribute('action', 'javascript:create();');
document.querySelectorAll('.switch-button').forEach(e => e.addEventListener('click', e => toggleWindow()));
forms.login.form.action = 'javascript:login();';
forms.create.form.action = 'javascript:create();';

View File

@@ -1,121 +1,118 @@
function fillNotificationSelection() {
const NotiEls = {
services_list: document.querySelector('#services-list'),
service_list: document.querySelector('#service-list'),
default_service_input: document.querySelector('#default-service-input'),
service_selection: document.querySelector('.notification-service-selection'),
notification_service_row: document.querySelector('.element-storage .notification-service-row'),
add_service_window: document.querySelector('#add-service-window'),
triggers: {
add_service: document.querySelector('#add-service-toggle'),
service_list: document.querySelector('#service-list-toggle')
}
};
//
// Fill lists and tables
//
function fillNotificationTable(json) {
NotiEls.services_list.innerHTML = '';
json.result.forEach(service => {
const entry = NotiEls.notification_service_row.cloneNode(true);
entry.dataset.id = service.id;
entry.querySelector('.title-column input').value = service.title;
const url_input = entry.querySelector('.url-column input');
url_input.value = service.url;
url_input.onkeydown = e => {
if (e.key === 'Enter')
saveService(service.id);
};
entry.querySelector('button[data-type="edit"]').onclick = e =>
document.querySelectorAll(`tr[data-id="${service.id}"] input`).forEach(
e => e.removeAttribute('readonly')
);
entry.querySelector('button[data-type="save"]').onclick = e =>
saveService(service.id);
entry.querySelector('button[data-type="delete"]').onclick = e =>
deleteService(service.id);
NotiEls.services_list.appendChild(entry);
});
};
function fillNotificationSelection(json) {
// Default service setting
NotiEls.default_service_input.innerHTML = '';
const default_service = getLocalStorage('default_service')['default_service'];
json.result.forEach(service => {
const entry = document.createElement('option');
entry.value = service.id;
entry.innerText = service.title;
if (default_service === service.id)
entry.setAttribute('selected', '');
NotiEls.default_service_input.appendChild(entry);
});
if (!NotiEls.default_service_input.querySelector(`option[value="${default_service}"]`))
setLocalStorage({'default_service':
parseInt(NotiEls.default_service_input.querySelector('option')?.value)
|| null
});
// Selection when managing (static)reminders/templates
NotiEls.service_selection.innerHTML = '';
json.result.forEach(service => {
const entry = document.createElement('div');
const select = document.createElement('input');
select.dataset.id = service.id;
select.type = 'checkbox';
entry.appendChild(select);
const title = document.createElement('p');
title.innerText = service.title;
entry.appendChild(title);
NotiEls.service_selection.appendChild(entry);
});
if (json.result.length > 0)
NotiEls.service_selection.querySelector(':first-child input').checked = true;
};
function setNoNotificationServiceMsg(json) {
if (json.result.length > 0) {
LibEls.tab_container.querySelectorAll('.add-entry').forEach(ae => {
ae.classList.remove('error', 'error-icon');
if (ae.id === 'add-reminder')
ae.onclick = e => showAdd(Types.reminder);
else if (ae.id === 'add-static-reminder')
ae.onclick = e => showAdd(Types.static_reminder);
else if (ae.id === 'add-template')
ae.onclick = e => showAdd(Types.template);
});
} else {
LibEls.tab_container.querySelectorAll('.add-entry').forEach(ae => {
ae.classList.add('error', 'error-icon');
ae.onclick = e => showWindow('notification');
});
};
};
function fillNotificationServices() {
fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`)
.then(response => {
if (!response.ok) return Promise.reject(response.status);
return response.json();
})
.then(json => {
if (json.result.length) {
document.getElementById('add-reminder').classList.remove('error', 'error-icon');
const default_select = document.querySelector('#default-service-input');
default_select.innerHTML = '';
let default_service = getLocalStorage('default_service')['default_service'];
json.result.forEach(service => {
const entry = document.createElement('option');
entry.value = service.id;
entry.innerText = service.title;
if (default_service === service.id)
entry.setAttribute('selected', '');
default_select.appendChild(entry);
});
if (!document.querySelector(`#default-service-input > option[value="${default_service}"]`))
setLocalStorage({'default_service':
parseInt(document.querySelector('#default-service-input > option')?.value)
|| null
});
default_service = getLocalStorage('default_service')['default_service'];
inputs.notification_service.innerHTML = '';
json.result.forEach(service => {
const entry = document.createElement('div');
const select = document.createElement('input');
select.dataset.id = service.id;
select.type = 'checkbox';
entry.appendChild(select);
const title = document.createElement('p');
title.innerText = service.title;
entry.appendChild(title);
inputs.notification_service.appendChild(entry);
});
inputs.notification_service.querySelector(':first-child input').checked = true;
const table = document.getElementById('services-list');
table.innerHTML = '';
json.result.forEach(service => {
const entry = document.createElement('tr');
entry.dataset.id = service.id;
const title_container = document.createElement('td');
title_container.classList.add('title-column');
const title = document.createElement('input');
title.setAttribute('readonly', '');
title.setAttribute('type', 'text');
title.value = service.title;
title_container.appendChild(title);
entry.appendChild(title_container);
const url_container = document.createElement('td');
url_container.classList.add('url-column');
const url = document.createElement('input');
url.setAttribute('readonly', '');
url.setAttribute('type', 'text');
url.value = service.url;
url.addEventListener('keydown', e => {
if (e.key === 'Enter')
saveService(service.id);
});
url_container.appendChild(url);
entry.appendChild(url_container);
const actions = document.createElement('td');
actions.classList.add('action-column');
entry.appendChild(actions);
const edit_button = document.createElement('button');
edit_button.dataset.type = 'edit';
edit_button.addEventListener('click', e => editService(service.id));
edit_button.title = 'Edit';
edit_button.setAttribute('aria-label', 'Edit');
edit_button.innerHTML = icons.edit;
actions.appendChild(edit_button);
const save_button = document.createElement('button');
save_button.dataset.type = 'save';
save_button.addEventListener('click', e => saveService(service.id));
save_button.title = 'Save Edits';
save_button.setAttribute('aria-label', 'Save Edits');
save_button.innerHTML = icons.save;
actions.appendChild(save_button);
const delete_button = document.createElement('button');
delete_button.dataset.type = 'delete';
delete_button.addEventListener('click', e => deleteService(service.id));
delete_button.title = 'Delete';
delete_button.setAttribute('aria-label', 'Delete');
delete_button.innerHTML = icons.delete;
actions.appendChild(delete_button);
table.appendChild(entry);
});
} else {
document.getElementById('add-reminder').classList.add('error', 'error-icon');
inputs.notification_service.innerHTML = '';
const default_select = document.querySelector('#default-service-input');
default_select.innerHTML = '';
const default_service = getLocalStorage('default_service')['default_service'];
if (!document.querySelector(`#default-service-input > option[value="${default_service}"]`))
setLocalStorage({'default_service':
parseInt(document.querySelector('#default-service-input > option')?.value)
|| null
});
};
fillNotificationTable(json);
fillNotificationSelection(json);
setNoNotificationServiceMsg(json);
})
.catch(e => {
if (e === 401)
@@ -125,11 +122,9 @@ function fillNotificationSelection() {
});
};
function editService(id) {
document.querySelectorAll(`tr[data-id="${id}"] input`).forEach(e => e.removeAttribute('readonly'));
document.querySelector(`tr[data-id="${id}"]`).classList.add('edit');
};
//
// Actions for table
//
function saveService(id) {
const row = document.querySelector(`tr[data-id="${id}"]`);
const save_button = row.querySelector('button[data-type="save"]');
@@ -144,8 +139,8 @@ function saveService(id) {
})
.then(response => {
if (!response.ok) return Promise.reject(response.status);
fillNotificationSelection();
fillNotificationServices();
})
.catch(e => {
if (e === 401)
@@ -153,7 +148,6 @@ function saveService(id) {
else if (e === 400) {
save_button.classList.add('error-icon');
save_button.title = 'Invalid Apprise URL';
save_button.setAttribute('aria-label', 'Invalid Apprise URL');
} else
console.log(e);
});
@@ -169,9 +163,7 @@ function deleteService(id) {
if (json.error !== null) return Promise.reject(json);
row.remove();
fillNotificationSelection();
if (document.querySelectorAll('#services-list > tr').length === 0)
document.getElementById('add-reminder').classList.add('error', 'error-icon');
fillNotificationServices();
})
.catch(e => {
if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid')
@@ -185,83 +177,31 @@ function deleteService(id) {
});
};
function testService() {
const test_button = document.querySelector('#test-service');
// Check regexes for input's
[...document.querySelectorAll('#add-service-window > input:not([data-regex=""])[data-regex]')]
.forEach(el => el.classList.remove('error-input'));
const faulty_inputs =
[...document.querySelectorAll('#add-service-window > input:not([data-regex=""])[data-regex]')]
.filter(el => !new RegExp
(
el.dataset.regex.split(',').slice(0, el.dataset.regex.split(',').length-1).join(','),
el.dataset.regex.split(',')[el.dataset.regex.split(',').length-1]
).test(el.value)
);
if (faulty_inputs.length > 0) {
faulty_inputs.forEach(el => el.classList.add('error-input'));
//
// Adding a service
//
function showServiceList(e) {
if (!e.target.checked)
return;
};
const data = {
'url': buildAppriseURL()
};
if (!data.url) {
test_button.classList.add('error-input');
test_button.title = 'Required field missing';
NotiEls.triggers.add_service.checked = false;
if (notification_services !== null)
return;
};
fetch(`${url_prefix}/api/notificationservices/test?api_key=${api_key}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
})
.then(response => {
if (!response.ok) return Promise.reject(response.status);
test_button.classList.remove('error-input');
test_button.title = '';
test_button.classList.add('show-sent');
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
else if (e === 400) {
test_button.classList.add('error-input');
test_button.title = 'Invalid Apprise URL';
} else
console.log(e);
fetch(`${url_prefix}/api/notificationservices/available?api_key=${api_key}`)
.then(response => response.json())
.then(json => {
notification_services = json.result;
json.result.forEach((result, index) => {
const entry = document.createElement('button');
entry.innerText = result.name;
entry.onclick = e => showAddServiceWindow(index);
NotiEls.service_list.appendChild(entry);
});
});
};
function toggleAddService() {
const cont = document.querySelector('.overflow-container');
if (cont.classList.contains('show-add')) {
// Hide add
cont.classList.remove('show-add');
hideAddServiceWindow();
} else {
// Show add
if (notification_services === null) {
fetch(`${url_prefix}/api/notificationservices/available?api_key=${api_key}`)
.then(response => response.json())
.then(json => {
notification_services = json.result;
const table = document.querySelector('#service-list');
json.result.forEach((result, index) => {
const entry = document.createElement('button');
entry.innerText = result.name;
entry.addEventListener('click', e => showAddServiceWindow(index));
table.appendChild(entry);
});
});
};
cont.classList.add('show-add');
};
};
function createTitle() {
const service_title = document.createElement('input');
service_title.id = 'service-title';
@@ -354,25 +294,25 @@ function createEntriesList(token) {
add_row.classList.add('add-row', 'hidden');
const add_input = document.createElement('input');
add_input.type = 'text';
add_input.addEventListener('keydown', e => {
add_input.onkeydown = e => {
if (e.key === "Enter") {
e.preventDefault();
e.stopImmediatePropagation();
addEntry(entries_list);
};
});
};
add_row.appendChild(add_input);
const add_entry_button = document.createElement('button');
add_entry_button.type = 'button';
add_entry_button.innerText = 'Add';
add_entry_button.addEventListener('click', e => addEntry(entries_list));
add_entry_button.onclick = e => addEntry(entries_list);
add_row.appendChild(add_entry_button);
entries_list.appendChild(add_row);
const add_button = document.createElement('button');
add_button.type = 'button';
add_button.innerHTML = icons.add;
add_button.addEventListener('click', e => toggleAddRow(add_row));
add_button.innerHTML = Icons.add;
add_button.onclick = e => toggleAddRow(add_row);
entries_list.appendChild(add_button);
return entries_list;
@@ -398,7 +338,7 @@ function addEntry(entries_list) {
};
function showAddServiceWindow(index) {
const window = document.getElementById('add-service-window');
const window = NotiEls.add_service_window;
window.innerHTML = '';
window.dataset.index = index;
@@ -423,10 +363,10 @@ function showAddServiceWindow(index) {
const show_args = document.createElement('button');
show_args.type = 'button';
show_args.innerText = 'Show Advanced Settings';
show_args.addEventListener('click', e => {
show_args.onclick = e => {
window.querySelectorAll('[data-is_arg="true"]').forEach(el => el.classList.toggle('hidden'));
show_args.innerText = show_args.innerText === 'Show Advanced Settings' ? 'Hide Advanced Settings' : 'Show Advanced Settings';
});
};
window.appendChild(show_args);
};
@@ -474,7 +414,9 @@ function showAddServiceWindow(index) {
});
if (vars[1] === 'args' && vars[0].length > 0)
window.querySelectorAll('[data-is_arg="true"]').forEach(el => el.classList.toggle('hidden'));
window.querySelectorAll('[data-is_arg="true"]').forEach(
el => el.classList.toggle('hidden')
);
})
// Bottom options
@@ -484,13 +426,13 @@ function showAddServiceWindow(index) {
const cancel = document.createElement('button');
cancel.type = 'button';
cancel.innerText = 'Cancel';
cancel.addEventListener('click', e => toggleAddService());
cancel.onclick = e => NotiEls.triggers.add_service.checked = false;
options.appendChild(cancel);
const test = document.createElement('button');
test.id = 'test-service';
test.type = 'button';
test.addEventListener('click', e => testService());
test.onclick = e => testService();
options.appendChild(test);
const test_text = document.createElement('div');
test_text.innerText = 'Test';
@@ -504,17 +446,13 @@ function showAddServiceWindow(index) {
add.innerText = 'Add';
options.appendChild(add);
window.appendChild(options);
document.getElementById('add-service-container').classList.add('show-add-window');
};
function hideAddServiceWindow() {
document.getElementById('add-service-container').classList.remove('show-add-window');
NotiEls.triggers.add_service.checked = true;
};
function buildAppriseURL() {
const data = notification_services[document.querySelector('#add-service-window').dataset.index];
const inputs = document.querySelectorAll('#add-service-window > [data-map][data-is_arg="false"]');
const data = notification_services[NotiEls.add_service_window.dataset.index];
const inputs = NotiEls.add_service_window.querySelectorAll('[data-map][data-is_arg="false"]');
const values = {};
// Gather all values and format
@@ -555,7 +493,7 @@ function buildAppriseURL() {
template = template.replace(`{${key}}`, value);
// Add args
const args = [...document.querySelectorAll('#add-service-window > [data-map][data-is_arg="true"]')]
const args = [...NotiEls.add_service_window.querySelectorAll('[data-map][data-is_arg="true"]')]
.map(el => {
if (['INPUT', 'SELECT'].includes(el.nodeName) && el.value && el.value !== el.dataset.default)
return `${el.dataset.map}=${el.value}`;
@@ -587,15 +525,66 @@ function buildAppriseURL() {
return template;
};
function addService() {
const add_button = document.querySelector('#add-service-window > .options > button[type="submit"]');
function testService() {
const test_button = document.querySelector('#test-service');
// Check regexes for input's
[...document.querySelectorAll('#add-service-window > input:not([data-regex=""])[data-regex]')]
[...NotiEls.add_service_window.querySelectorAll('input:not([data-regex=""])[data-regex]')]
.forEach(el => el.classList.remove('error-input'));
const faulty_inputs =
[...document.querySelectorAll('#add-service-window > input:not([data-regex=""])[data-regex]')]
[...NotiEls.add_service_window.querySelectorAll('input:not([data-regex=""])[data-regex]')]
.filter(el => !new RegExp
(
el.dataset.regex.split(',').slice(0, el.dataset.regex.split(',').length-1).join(','),
el.dataset.regex.split(',')[el.dataset.regex.split(',').length-1]
).test(el.value)
);
if (faulty_inputs.length > 0) {
faulty_inputs.forEach(el => el.classList.add('error-input'));
return;
};
const data = {
'url': buildAppriseURL()
};
if (!data.url) {
test_button.classList.add('error-input');
test_button.title = 'Required field missing';
return;
};
fetch(`${url_prefix}/api/notificationservices/test?api_key=${api_key}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
})
.then(response => {
if (!response.ok) return Promise.reject(response.status);
test_button.classList.remove('error-input');
test_button.title = '';
test_button.classList.add('show-sent');
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
else if (e === 400) {
test_button.classList.add('error-input');
test_button.title = 'Invalid Apprise URL';
} else
console.log(e);
});
};
function addService() {
const add_button = NotiEls.add_service_window.querySelector('.options > button[type="submit"]');
// Check regexes for input's
[...NotiEls.add_service_window.querySelectorAll('input:not([data-regex=""])[data-regex]')]
.forEach(el => el.classList.remove('error-input'));
const faulty_inputs =
[...NotiEls.add_service_window.querySelectorAll('input:not([data-regex=""])[data-regex]')]
.filter(el =>
!(
(!el.required && el.value === '')
@@ -631,8 +620,9 @@ function addService() {
add_button.classList.remove('error-input');
add_button.title = '';
toggleAddService();
fillNotificationSelection();
NotiEls.triggers.service_list.checked = false;
NotiEls.triggers.add_service.checked = false;
fillNotificationServices();
})
.catch(e => {
if (e === 401)
@@ -647,9 +637,9 @@ function addService() {
// code run on load
fillNotificationSelection();
fillNotificationServices();
let notification_services = null;
document.getElementById('add-service-button').addEventListener('click', e => toggleAddService());
document.getElementById('add-service-window').setAttribute('action', 'javascript:addService();');
NotiEls.triggers.service_list.onchange = showServiceList;
NotiEls.add_service_window.action = 'javascript:addService();';

View File

@@ -1,14 +1,24 @@
const SettingsEls = {
locale_input: document.querySelector('#locale-input'),
default_service_input: document.querySelector('#default-service-input'),
change_password_form: document.querySelector('#change-password-form'),
delete_account_button: document.querySelector('#delete-account-button')
};
function loadSettings() {
document.getElementById('locale-input').value = getLocalStorage('locale')['locale'];
// Default Service is handled by notification.fillNotificationSelection()
document.getElementById('locale-input').value =
getLocalStorage('locale')['locale'];
};
function updateLocale(e) {
setLocalStorage({'locale': e.target.value});
window.location.reload();
fillLibrary(Types.reminder);
};
function updateDefaultService(e) {
setLocalStorage({'default_service': parseInt(e.target.value)});
// Add window is handled by show.showAdd()
};
function changePassword() {
@@ -45,7 +55,7 @@ function deleteAccount() {
loadSettings();
document.getElementById('locale-input').addEventListener('change', updateLocale);
document.querySelector('#default-service-input').addEventListener('change', updateDefaultService);
document.getElementById('change-password-form').setAttribute('action', 'javascript:changePassword()');
document.getElementById('delete-account-button').addEventListener('click', e => deleteAccount());
SettingsEls.locale_input.onchange = updateLocale;
SettingsEls.default_service_input.onchange = updateDefaultService;
SettingsEls.change_password_form.action = 'javascript:changePassword();';
SettingsEls.delete_account_button.onclick = e => deleteAccount();

View File

@@ -4,32 +4,37 @@ function showAdd(type) {
inputs.title.value = '';
inputs.text.value = '';
inputs.time.value = '';
inputs.notification_service.querySelectorAll('input[type="checkbox"]').forEach(c => c.checked = false);
inputs.notification_service.querySelector(`input[type="checkbox"][data-id="${default_service}"]`).checked = true;
document.querySelectorAll('.weekday-bar > input[type="checkbox"]').forEach(el => el.checked = false);
inputs.notification_service.querySelectorAll('input[type="checkbox"]')
.forEach(c => c.checked = false);
inputs.notification_service.querySelector(
`input[type="checkbox"][data-id="${default_service}"]`
).checked = true;
document.querySelectorAll('.weekday-bar > input[type="checkbox"]')
.forEach(el => el.checked = false);
toggleNormal();
toggleColor(true);
selectColor(colors[0]);
inputs.color_toggle.checked = false;
document.getElementById('test-reminder').classList.remove('show-sent');
const cl = document.getElementById('info').classList;
cl.forEach(c => {
if (info_classes.includes(c)) cl.remove(c)
if (InfoClasses.includes(c)) cl.remove(c)
});
document.querySelector('.options > button[type="submit"]').innerText = 'Add';
if (type === types.reminder) {
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
const title = document.querySelector('#info h2');
if (type === Types.reminder) {
cl.add('show-add-reminder');
document.querySelector('#info h2').innerText = 'Add a reminder';
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
title.innerText = 'Add a reminder';
inputs.time.setAttribute('required', '');
} else if (type === types.template) {
} else if (type === Types.template) {
cl.add('show-add-template');
document.querySelector('#info h2').innerText = 'Add a template';
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
title.innerText = 'Add a template';
inputs.time.removeAttribute('required');
} else if (type === types.static_reminder) {
} else if (type === Types.static_reminder) {
cl.add('show-add-static-reminder');
document.querySelector('#info h2').innerText = 'Add a static reminder';
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
title.innerText = 'Add a static reminder';
inputs.time.removeAttribute('required');
} else
return;
@@ -38,14 +43,14 @@ function showAdd(type) {
function showEdit(id, type) {
let url;
if (type === types.reminder) {
if (type === Types.reminder) {
url = `${url_prefix}/api/reminders/${id}?api_key=${api_key}`;
inputs.time.setAttribute('required', '');
} else if (type === types.template) {
} else if (type === Types.template) {
url = `${url_prefix}/api/templates/${id}?api_key=${api_key}`;
inputs.time.removeAttribute('required');
type_buttons.repeat_interval.removeAttribute('required');
} else if (type === types.static_reminder) {
} else if (type === Types.static_reminder) {
url = `${url_prefix}/api/staticreminders/${id}?api_key=${api_key}`;
document.getElementById('test-reminder').classList.remove('show-sent');
inputs.time.removeAttribute('required');
@@ -59,26 +64,27 @@ function showEdit(id, type) {
})
.then(json => {
document.getElementById('info').dataset.id = id;
if (json.result.color !== null) {
if (inputs.color.classList.contains('hidden')) {
toggleColor();
};
selectColor(json.result['color']);
};
inputs.color_toggle.checked = false;
selectColor(json.result.color || colors[0]);
inputs.title.value = json.result.title;
if (type === types.reminder) {
if (type === Types.reminder) {
var trigger_date = new Date(
(json.result.time + new Date(json.result.time * 1000).getTimezoneOffset() * -60) * 1000
(json.result.time
+ new Date(json.result.time * 1000).getTimezoneOffset()
* -60
) * 1000
);
inputs.time.value = trigger_date.toLocaleString('en-CA').slice(0,10) + 'T' + trigger_date.toTimeString().slice(0,5);
inputs.time.value =
trigger_date.toLocaleString('en-CA').slice(0,10)
+ 'T'
+ trigger_date.toTimeString().slice(0,5);
};
inputs.notification_service.querySelectorAll('input[type="checkbox"]').forEach(
c => c.checked = json.result.notification_services.includes(parseInt(c.dataset.id))
);
if (type == types.reminder) {
if (type == Types.reminder) {
if (json.result.repeat_interval !== null) {
toggleRepeated();
type_buttons.repeat_interval.value = json.result.repeat_interval;
@@ -106,32 +112,23 @@ function showEdit(id, type) {
const cl = document.getElementById('info').classList;
cl.forEach(c => {
if (info_classes.includes(c)) cl.remove(c)
if (InfoClasses.includes(c)) cl.remove(c)
});
document.querySelector('.options > button[type="submit"]').innerText = 'Save';
if (type === types.reminder) {
const title = document.querySelector('#info h2');
const test_text = document.querySelector('#test-reminder > div:first-child');
if (type === Types.reminder) {
cl.add('show-edit-reminder');
document.querySelector('#info h2').innerText = 'Edit a reminder';
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
} else if (type === types.template) {
title.innerText = 'Edit a reminder';
test_text.innerText = 'Test';
} else if (type === Types.template) {
cl.add('show-edit-template');
document.querySelector('#info h2').innerText = 'Edit a template';
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
} else if (type === types.static_reminder) {
title.innerText = 'Edit a template';
test_text.innerText = 'Test';
} else if (type === Types.static_reminder) {
cl.add('show-edit-static-reminder');
document.querySelector('#info h2').innerText = 'Edit a static reminder';
document.querySelector('#test-reminder > div:first-child').innerText = 'Trigger';
title.innerText = 'Edit a static reminder';
test_text.innerText = 'Trigger';
} else
return;
};
// code run on load
document.getElementById('add-reminder').addEventListener('click', e => {
if (document.getElementById('add-reminder').classList.contains('error'))
showWindow('notification');
else
showAdd(types.reminder);
});
document.getElementById('add-static-reminder').addEventListener('click', e => showAdd(types.static_reminder));
document.getElementById('add-template').addEventListener('click', e => showAdd(types.template));

View File

@@ -26,9 +26,12 @@ function loadTemplateSelection() {
function applyTemplate() {
if (inputs.template.value === '0') {
inputs.title.value = '';
inputs.notification_service.querySelectorAll('input[type="checkbox"]:checked').forEach(c => c.checked = false)
inputs.notification_service.querySelectorAll(
'input[type="checkbox"]:checked'
).forEach(c => c.checked = false)
inputs.text.value = '';
toggleColor(true);
selectColor(colors[0]);
} else {
fetch(`${url_prefix}/api/templates/${inputs.template.value}?api_key=${api_key}`)
.then(response => {
@@ -41,12 +44,7 @@ function applyTemplate() {
c => c.checked = json.result.notification_services.includes(parseInt(c.dataset.id))
);
inputs.text.value = json.result.text;
if (json.result.color !== null) {
if (inputs.color.classList.contains('hidden'))
toggleColor();
selectColor(json.result.color);
} else
toggleColor(true);
selectColor(json.result.color || colors[0]);
})
.catch(e => {
if (e === 401)

View File

@@ -1,57 +1,54 @@
const colors = ["#3c3c3c", "#49191e", "#171a42", "#083b06", "#3b3506", "#300e40"];
const inputs = {
'template': document.getElementById('template-selection'),
'title': document.getElementById('title-input'),
'time': document.getElementById('time-input'),
'notification_service': document.querySelector('.notification-service-list'),
'text': document.getElementById('text-input'),
'color': document.querySelector('.color-list')
'template': document.querySelector('#template-selection'),
'color_toggle': document.querySelector('#color-toggle'),
'color_button': document.querySelector('#color-button'),
'color': document.querySelector('.color-list'),
'title': document.querySelector('#title-input'),
'time': document.querySelector('#time-input'),
'notification_service': document.querySelector('.notification-service-selection'),
'text': document.querySelector('#text-input'),
};
const type_buttons = {
'normal_button': document.getElementById('normal-button'),
'repeat_button': document.getElementById('repeat-button'),
'weekday_button': document.getElementById('weekday-button'),
'normal_button': document.querySelector('#normal-button'),
'repeat_button': document.querySelector('#repeat-button'),
'weekday_button': document.querySelector('#weekday-button'),
'repeat_bar': document.querySelector('.repeat-bar'),
'repeat_interval': document.getElementById('repeat-interval'),
'repeat_quantity': document.getElementById('repeat-quantity'),
'repeat_interval': document.querySelector('#repeat-interval'),
'repeat_quantity': document.querySelector('#repeat-quantity'),
'weekday_bar': document.querySelector('.weekday-bar')
};
function loadColor() {
colors.forEach(color => {
const entry = document.createElement('button');
entry.dataset.color = color;
entry.title = color;
entry.type = 'button';
entry.style.setProperty('--color', color);
entry.addEventListener('click', e => selectColor(color))
inputs.color.appendChild(entry);
function fillColors() {
colors.forEach((color, idx) => {
const entry_toggle = document.createElement('label');
entry_toggle.title = color;
entry_toggle.style.setProperty('--color', color);
inputs.color.appendChild(entry_toggle);
const entry = document.createElement('input');
entry.type = 'radio';
entry.name = 'color_selection';
entry.value = color;
entry.checked = idx === 0;
entry.classList.add('hidden');
entry.onchange = e => {
if (e.target === entry)
inputs.color_button.style.setProperty('--color', color);
};
entry_toggle.appendChild(entry);
if (idx === 0)
inputs.color_button.style.setProperty('--color', color);
});
};
function selectColor(color_code) {
inputs.color.querySelector(`button[data-color="${color_code}"]`).dataset.selected = 'true';
inputs.color.querySelectorAll(`button:not([data-color="${color_code}"])`).forEach(b => b.dataset.selected = 'false');
return;
};
function toggleColor(hide=false) {
selectColor(colors[0])
if (!hide)
inputs.color.classList.toggle('hidden');
else
inputs.color.classList.add('hidden');
};
function toggleNotificationService(hide=false) {
if (!hide)
inputs.notification_service.classList.toggle('hidden');
else
inputs.notification_service.classList.add('hidden');
inputs.color.querySelector(`label[title="${color_code}"]`).click();
};
function toggleNormal() {
@@ -64,7 +61,8 @@ function toggleNormal() {
type_buttons.repeat_interval.value = '';
type_buttons.weekday_bar.classList.add('hidden');
type_buttons.weekday_bar.querySelectorAll('input[type="checkbox"]').forEach(el => el.checked = false);
type_buttons.weekday_bar.querySelectorAll('input[type="checkbox"]')
.forEach(el => el.checked = false);
};
function toggleRepeated() {
@@ -76,7 +74,8 @@ function toggleRepeated() {
type_buttons.repeat_interval.setAttribute('required', '');
type_buttons.weekday_bar.classList.add('hidden');
type_buttons.weekday_bar.querySelectorAll('input[type="checkbox"]').forEach(el => el.checked = false);
type_buttons.weekday_bar.querySelectorAll('input[type="checkbox"]')
.forEach(el => el.checked = false);
};
function toggleWeekDay() {
@@ -115,7 +114,7 @@ function testReminder() {
};
const ns = [...
document.querySelectorAll('.notification-service-list input[type="checkbox"]:checked')
document.querySelectorAll('.notification-service-selection input[type="checkbox"]:checked')
].map(c => parseInt(c.dataset.id))
if (!ns.length) {
input.classList.add('error-input');
@@ -168,16 +167,16 @@ function deleteInfo() {
if (cl.contains('show-edit-reminder')) {
// Delete reminder
fillReminders();
fillLibrary(Types.reminder);
} else if (cl.contains('show-edit-template')) {
// Delete template
fillTemplates();
fillLibrary(Types.template);
loadTemplateSelection();
} else if (cl.contains('show-edit-static-reminder')) {
// Delete static reminder
fillStaticReminders();
fillLibrary(Types.static_reminder);
};
hideWindow();
showWindow("home");
})
.catch(e => {
if (e === 401)
@@ -202,14 +201,13 @@ function submitInfo() {
const data = {
'title': inputs.title.value,
'notification_services': [...
document.querySelectorAll('.notification-service-list input[type="checkbox"]:checked')
document.querySelectorAll('.notification-service-selection input[type="checkbox"]:checked')
].map(c => parseInt(c.dataset.id)),
'text': inputs.text.value,
'color': null
};
if (!inputs.color.classList.contains('hidden')) {
data['color'] = inputs.color.querySelector('button[data-selected="true"]').dataset.color;
'color': inputs.color.querySelector('input:checked').value
};
if (data.color === colors[0])
data.color = null;
if (data.notification_services.length === 0) {
inputs.notification_service.classList.add('error-input');
@@ -221,7 +219,10 @@ function submitInfo() {
const cl = document.getElementById('info').classList;
if (cl.contains('show-add-reminder')) {
// Add reminder
data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60)
data['time'] =
(new Date(inputs.time.value) / 1000)
+ (new Date(inputs.time.value).getTimezoneOffset() * 60);
if (type_buttons.repeat_button.dataset.selected === 'true') {
data['repeat_quantity'] = type_buttons.repeat_quantity.value;
data['repeat_interval'] = parseInt(type_buttons.repeat_interval.value)
@@ -242,7 +243,7 @@ function submitInfo() {
fetch_data.url = `${url_prefix}/api/reminders?api_key=${api_key}`;
fetch_data.method = 'POST';
fetch_data.call_back = fillReminders;
fetch_data.call_back = () => fillLibrary(Types.reminder);
} else if (cl.contains('show-add-template')) {
// Add template
@@ -250,18 +251,21 @@ function submitInfo() {
fetch_data.method = 'POST';
fetch_data.call_back = () => {
loadTemplateSelection();
fillTemplates();
fillLibrary(Types.template);
};
} else if (cl.contains('show-add-static-reminder')) {
// Add static reminder
fetch_data.url = `${url_prefix}/api/staticreminders?api_key=${api_key}`;
fetch_data.method = 'POST';
fetch_data.call_back = fillStaticReminders;
fetch_data.call_back = () => fillLibrary(Types.static_reminder);
} else if (cl.contains('show-edit-reminder')) {
// Edit reminder
data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60)
data['time'] =
(new Date(inputs.time.value) / 1000)
+ (new Date(inputs.time.value).getTimezoneOffset() * 60);
if (type_buttons.repeat_button.dataset.selected === 'true') {
data['repeat_quantity'] = type_buttons.repeat_quantity.value;
data['repeat_interval'] = parseInt(type_buttons.repeat_interval.value)
@@ -282,7 +286,7 @@ function submitInfo() {
fetch_data.url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`;
fetch_data.method = 'PUT';
fetch_data.call_back = fillReminders;
fetch_data.call_back = () => fillLibrary(Types.reminder);
} else if (cl.contains('show-edit-template')) {
// Edit template
@@ -290,14 +294,14 @@ function submitInfo() {
fetch_data.method = 'PUT';
fetch_data.call_back = () => {
loadTemplateSelection();
fillTemplates();
fillLibrary(Types.template);
};
} else if (cl.contains('show-edit-static-reminder')) {
// Edit a static reminder
fetch_data.url = `${url_prefix}/api/staticreminders/${e_id}?api_key=${api_key}`;
fetch_data.method = 'PUT';
fetch_data.call_back = fillStaticReminders;
fetch_data.call_back = () => fillLibrary(Types.static_reminder);
} else return;
@@ -310,7 +314,7 @@ function submitInfo() {
if (!response.ok) return Promise.reject(response.status);
fetch_data.call_back()
hideWindow();
showWindow("home");
})
.catch(e => {
if (e === 401) {
@@ -325,15 +329,13 @@ function submitInfo() {
// code run on load
loadColor();
fillColors();
document.getElementById('template-selection').addEventListener('change', e => applyTemplate());
document.getElementById('color-toggle').addEventListener('click', e => toggleColor());
document.getElementById('toggle-notification-service-list').addEventListener('click', e => toggleNotificationService());
document.getElementById('normal-button').addEventListener('click', e => toggleNormal());
document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated());
document.getElementById('weekday-button').addEventListener('click', e => toggleWeekDay());
document.getElementById('close-info').addEventListener('click', e => hideWindow());
document.getElementById('delete-info').addEventListener('click', e => deleteInfo());
document.getElementById('test-reminder').addEventListener('click', e => testReminder());
document.getElementById('info-form').setAttribute('action', 'javascript:submitInfo();');
document.querySelector('#template-selection').onchange = e => applyTemplate();
document.querySelector('#normal-button').onclick = e => toggleNormal();
document.querySelector('#repeat-button').onclick = e => toggleRepeated();
document.querySelector('#weekday-button').onclick = e => toggleWeekDay();
document.querySelector('#close-info').onclick = e => showWindow("home");
document.querySelector('#delete-info').onclick = e => deleteInfo();
document.querySelector('#test-reminder').onclick = e => testReminder();
document.querySelector('#info-form').action = 'javascript:submitInfo();';

View File

@@ -39,7 +39,7 @@
<div class="form-container">
<form id="settings-form">
<h2>Authentication</h2>
<div class="table-container">
<div class="settings-table-container">
<table class="settings-table">
<tbody>
<tr>
@@ -86,24 +86,34 @@
</svg>
</button>
</div>
<div class="table-container">
<div class="user-table-container">
<form id="add-user-form"></form>
<table id="user-table">
<thead>
<th>User</th>
<th></th>
<th>Actions</th>
</thead>
<tbody id="add-user-row" class="hidden">
<form id="add-user-form">
<tr>
<td>
<input type="text" id="new-username-input" placeholder="Username">
<input type="password" id="new-password-input" placeholder="Password">
</td>
<td>
<button type="submit">Add</button>
</td>
</tr>
</form>
<tr>
<td>
<input type="text" id="new-username-input" form="add-user-form" placeholder="Username" required>
</td>
<td>
<input type="password" id="new-password-input" form="add-user-form" placeholder="Password" required>
</td>
<td>
<button type="submit" form="add-user-form">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</g>
</svg>
</button>
</td>
</tr>
</tbody>
<tbody id="user-list">
</tbody>

View File

@@ -19,18 +19,25 @@
<img src="{{ url_for('static', filename='img/favicon.svg') }}" alt="">
</header>
<main>
<input type="checkbox" id="form-switch" class="hidden">
<div class="form-container">
<form id="login-form">
<h2>Login</h2>
<noscript>Javascript is disabled. The web-ui of MIND does not work with JavaScript disabled.</noscript>
<input type="text" autocomplete="username" placeholder="Username" required autofocus>
<p class="error hidden" id="username-error">*Username not found</p>
<div id="username-error-container">
<input type="text" autocomplete="username" placeholder="Username" required autofocus>
<p class="hidden">*Username not found</p>
</div>
<input type="password" autocomplete="current-password" placeholder="Password" required>
<p class="error hidden" id="password-error">*Password incorrect</p>
<div id="password-error-container">
<input type="password" autocomplete="current-password" placeholder="Password" required>
<p class="hidden">*Password incorrect</p>
</div>
<button type="button" class="switch-button">Or create an account</button>
<label for="form-switch" class="switch-button">Or create an account</label>
<button type="submit">Login</button>
</form>
</div>
@@ -44,7 +51,7 @@
<input type="password" autocomplete="new-password" placeholder="Password" required>
<button type="button" class="switch-button">Or log into an account</button>
<label for="form-switch" class="switch-button">Or log into an account</label>
<button type="submit">Create</button>
</form>
</div>

View File

@@ -7,8 +7,8 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/x-icon">
<link rel="stylesheet" href="/static/css/general.css">
<link rel="stylesheet" href="/static/css/page_not_found.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/general.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/page_not_found.css') }}">
<title>Page Not Found - MIND</title>
</head>

View File

@@ -24,9 +24,56 @@
<title>Reminders - MIND</title>
</head>
<body>
<input type="checkbox" class="hidden" id="wide-toggle">
<div class="hidden element-storage">
<table>
<tbody>
<tr class="notification-service-row" data-id="">
<td class="title-column">
<input type="text" readonly>
</td>
<td class="url-column">
<input type="text" readonly>
</td>
<td class="action-column">
<button data-type="edit" title="Edit">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M22.94,1.06a3.626,3.626,0,0,0-5.124,0L0,18.876V24H5.124L22.94,6.184A3.627,3.627,0,0,0,22.94,1.06ZM4.3,22H2V19.7L15.31,6.4l2.3,2.3ZM21.526,4.77,19.019,7.277l-2.295-2.3L19.23,2.474a1.624,1.624,0,0,1,2.3,2.3Z"></path>
</g>
</g>
</svg>
</button>
<button data-type="save" title="Save Edits">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M12,10a4,4,0,1,0,4,4A4,4,0,0,0,12,10Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,16Z"></path>
<path d="M22.536,4.122,19.878,1.464A4.966,4.966,0,0,0,16.343,0H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H19a5.006,5.006,0,0,0,5-5V7.657A4.966,4.966,0,0,0,22.536,4.122ZM17,2.08V3a3,3,0,0,1-3,3H10A3,3,0,0,1,7,3V2h9.343A2.953,2.953,0,0,1,17,2.08ZM22,19a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2V3a5.006,5.006,0,0,0,5,5h4a4.991,4.991,0,0,0,4.962-4.624l2.16,2.16A3.02,3.02,0,0,1,22,7.657Z"></path>
</g>
</svg>
</button>
<button data-type="delete" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M22,4H17V2a2,2,0,0,0-2-2H9A2,2,0,0,0,7,2V4H2V6H4V21a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V6h2ZM9,2h6V4H9Zm9,19a1,1,0,0,1-1,1H7a1,1,0,0,1-1-1V6H18Z"></path>
<rect x="9" y="10" width="2" height="8"></rect>
<rect x="13" y="10" width="2" height="8"></rect>
</g>
</g>
</svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<header>
<div>
<button id="toggle-nav">
<input type="checkbox" id="nav-switch" class="hidden">
<label for="nav-switch" id="toggle-nav">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<rect y="11" width="24" height="2" rx="1"></rect>
@@ -34,7 +81,7 @@
<rect y="18" width="24" height="2" rx="1"></rect>
</g>
</svg>
</button>
</label>
<img src="{{ url_for('static', filename='img/favicon.svg') }}" alt="">
</div>
</header>
@@ -79,9 +126,12 @@
<main class="window-container">
<div id="home">
<div class="tab-selector">
<button data-target="reminder-tab" data-selected="true">Reminders</button>
<button data-target="static-reminder-tab">Static Reminders</button>
<button data-target="template-tab">Templates</button>
<input type="radio" name="tab-selector" id="reminder-tab-selector" checked>
<label for="reminder-tab-selector">Reminders</label>
<input type="radio" name="tab-selector" id="static-tab-selector">
<label for="static-tab-selector">Static Reminders</label>
<input type="radio" name="tab-selector" id="template-tab-selector">
<label for="template-tab-selector">Templates</label>
</div>
<div class="search-container">
<noscript>Javascript is disabled. The web-ui of MIND does not work with JavaScript disabled.</noscript>
@@ -106,251 +156,286 @@
</g>
</svg>
</button>
<select id="sort-input"></select>
<select id="sort-input">
<option value="time">Time</option>
<option value="time_reversed">Time Reversed</option>
<option value="title">Title</option>
<option value="title_reversed">Title Reversed</option>
<option value="date_added">Date Added</option>
<option value="date_added_reversed">Date Added Reversed</option>
</select>
<label id="wide-button" for="wide-toggle" aria-hidden="true" title="Toggle wide list">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512">
<path d="M23.312,10.012c-.29-.327-.574-.637-.771-.834L19.713,6.3a1,1,0,0,0-1.426,1.4l2.834,2.885c.108.108.244.255.389.414H2.555c.146-.16.284-.308.4-.42L5.779,7.7A1,1,0,0,0,4.353,6.3L1.53,9.172c-.2.2-.487.513-.777.84A2.99,2.99,0,0,0,0,11.994v.012a3,3,0,0,0,.754,1.983c.289.326.573.636.769.833L4.353,17.7a1,1,0,0,0,1.426-1.4L2.944,13.414c-.108-.108-.244-.255-.389-.414H21.51c-.145.16-.283.308-.4.42L18.287,16.3a1,1,0,1,0,1.426,1.4l2.822-2.873c.2-.2.486-.513.777-.84A3,3,0,0,0,23.312,10.012Z"/>
</svg>
</label>
</div>
</form>
</div>
<div id="reminder-tab">
<button class="entry add-entry" id="add-reminder" aria-label="Add reminder" title="Add reminder">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<div class="tab-container">
<div id="reminder-tab">
<button class="entry add-entry" id="add-reminder" title="Add reminder">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</g>
</svg>
<p>Add a notification service first!</p>
</button>
</div>
<div id="static-reminder-tab" class="hidden">
<button class="entry add-entry" id="add-static-reminder" aria-label="Add static reminder" title="Add static reminder">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</g>
</svg>
</button>
</div>
<div id="template-tab" class="hidden">
<button class="entry add-entry" id="add-template" aria-label="Add template" title="Add template">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</g>
</svg>
</button>
</div>
</div>
<div id="info" class="hidden">
<h2></h2>
<div class="form-container">
<form id="info-form">
<div class="sub-inputs">
<select id="template-selection">
<option value="0" selected>No template</option>
</select>
<button type="button" id="color-toggle">Color</button>
</div>
<div class="color-list hidden"></div>
<input type="text" id="title-input" placeholder="Title" required>
<div class="sub-inputs">
<input type="datetime-local" id="time-input" required>
<button type="button" id="toggle-notification-service-list">Notification Services</button>
</div>
<div class="notification-service-list hidden"></div>
<div class="repeat-options sub-inputs">
<button type="button" id="normal-button" data-selected="true">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</svg>
Normal
</button>
<button type="button" id="repeat-button" data-selected="false">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
</g>
</svg>
Repeated
</button>
<button type="button" id="weekday-button" data-selected="false">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
</g>
</svg>
Week Days
</button>
</div>
<div class="repeat-bar hidden">
<p>Repeat every </p>
<input type="number" id="repeat-interval" placeholder="interval" min="1" step="1" oninput="validity.valid || (value='');">
<select id="repeat-quantity">
<option value="minutes">Minute(s)</option>
<option value="hours">Hour(s)</option>
<option value="days" selected>Day(s)</option>
<option value="weeks">Week(s)</option>
<option value="months">Month(s)</option>
<option value="years">Year(s)</option>
</select>
</div>
<div class="weekday-bar hidden">
<p>Mo</p>
<input type="checkbox">
<p>Tu</p>
<input type="checkbox">
<p>We</p>
<input type="checkbox">
<p>Thu</p>
<input type="checkbox">
<p>Fr</p>
<input type="checkbox">
<p>Sa</p>
<input type="checkbox">
<p>Su</p>
<input type="checkbox">
</div>
<textarea id="text-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
<div class="options">
<button type="button" id="close-info">Cancel</button>
<button type="button" id="test-reminder">
<div>Test</div>
<div>Sent</div>
</button>
<button type="button" id="delete-info">Delete</button>
<button type="submit">Add</button>
</div>
</form>
</div>
</div>
<div id="notification" class="hidden">
<h2>Notification Services</h2>
<p>Setup your notification providers here</p>
<div class="table-container">
<button id="add-service-button" title="Add notification service" aria-label="Add notification service">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</g>
</svg>
</button>
<div class="overflow-container">
<table>
<thead>
<tr>
<th class="title-column">Title</th>
<th class="url-column">Apprise URL</th>
<th title="Actions" aria-label="Actions" class="action-column">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M11.24,24a2.262,2.262,0,0,1-.948-.212,2.18,2.18,0,0,1-1.2-2.622L10.653,16H6.975A3,3,0,0,1,4.1,12.131l3.024-10A2.983,2.983,0,0,1,10,0h3.693a2.6,2.6,0,0,1,2.433,3.511L14.443,8H17a3,3,0,0,1,2.483,4.684l-6.4,10.3A2.2,2.2,0,0,1,11.24,24ZM10,2a1,1,0,0,0-.958.71l-3.024,10A1,1,0,0,0,6.975,14H12a1,1,0,0,1,.957,1.29L11.01,21.732a.183.183,0,0,0,.121.241A.188.188,0,0,0,11.4,21.9l6.4-10.3a1,1,0,0,0,.078-1.063A.979.979,0,0,0,17,10H13a1,1,0,0,1-.937-1.351l2.19-5.84A.6.6,0,0,0,13.693,2Z"></path>
</g>
</svg>
</th>
</tr>
</thead>
<tbody id="services-list">
</tbody>
</table>
<div id="add-service-container">
<div id="service-list">
</div>
<form id="add-service-window"></form>
</div>
</svg>
<p>Add a notification service first!</p>
</button>
</div>
<div id="static-reminder-tab">
<button class="entry add-entry" id="add-static-reminder" title="Add static reminder">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</g>
</svg>
<p>Add a notification service first!</p>
</button>
</div>
<div id="template-tab">
<button class="entry add-entry" id="add-template" title="Add template">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</g>
</svg>
<p>Add a notification service first!</p>
</button>
</div>
</div>
</div>
<div id="settings" class="hidden">
<h2>Settings</h2>
<div class="settings-container">
<h3>Locale</h3>
<select id="locale-input">
<option value="ar-SA">ar-SA</option>
<option value="bn-BD">bn-BD</option>
<option value="bn-IN">bn-IN</option>
<option value="cs-CZ">cs-CZ</option>
<option value="da-DK">da-DK</option>
<option value="de-AT">de-AT</option>
<option value="de-CH">de-CH</option>
<option value="de-DE">de-DE</option>
<option value="el-GR">el-GR</option>
<option value="en-AU">en-AU</option>
<option value="en-CA">en-CA</option>
<option value="en-GB" selected>en-GB</option>
<option value="en-IE">en-IE</option>
<option value="en-IN">en-IN</option>
<option value="en-NZ">en-NZ</option>
<option value="en-US">en-US</option>
<option value="en-ZA">en-ZA</option>
<option value="es-AR">es-AR</option>
<option value="es-CL">es-CL</option>
<option value="es-CO">es-CO</option>
<option value="es-ES">es-ES</option>
<option value="es-MX">es-MX</option>
<option value="es-US">es-US</option>
<option value="fi-FI">fi-FI</option>
<option value="fr-BE">fr-BE</option>
<option value="fr-CA">fr-CA</option>
<option value="fr-CH">fr-CH</option>
<option value="fr-FR">fr-FR</option>
<option value="he-IL">he-IL</option>
<option value="hi-IN">hi-IN</option>
<option value="hu-HU">hu-HU</option>
<option value="id-ID">id-ID</option>
<option value="it-CH">it-CH</option>
<option value="it-IT">it-IT</option>
<option value="ja-JP">ja-JP</option>
<option value="ko-KR">ko-KR</option>
<option value="nl-BE">nl-BE</option>
<option value="nl-NL">nl-NL</option>
<option value="no-NO">no-NO</option>
<option value="pl-PL">pl-PL</option>
<option value="pt-BR">pt-BR</option>
<option value="pt-PT">pt-PT</option>
<option value="ro-RO">ro-RO</option>
<option value="ru-RU">ru-RU</option>
<option value="sk-SK">sk-SK</option>
<option value="sv-SE">sv-SE</option>
<option value="ta-IN">ta-IN</option>
<option value="ta-LK">ta-LK</option>
<option value="th-TH">th-TH</option>
<option value="tr-TR">tr-TR</option>
<option value="zh-CN">zh-CN</option>
<option value="zh-HK">zh-HK</option>
<option value="zh-TW">zh-TW</option>
</select>
<h3>Default Notification Service</h3>
<select id="default-service-input"></select>
<h3>Change Password</h3>
<form id="change-password-form">
<input type="password" id="password-input" autocomplete="new-password" required>
<button type="submit">Change</button>
</form>
<h3>Delete Account</h3>
<button id="delete-account-button">Delete Account</button>
<div class="extra-window-container">
<div id="info">
<h2></h2>
<div class="form-container">
<form id="info-form">
<input type="checkbox" id="color-toggle" class="hidden">
<input type="checkbox" id="notification-service-selection-toggle" class="hidden">
<div class="sub-inputs">
<select id="template-selection">
<option value="0" selected>No template</option>
</select>
<label for="color-toggle" id="color-button" class="as-button">Color</label>
</div>
<div class="color-list"></div>
<input type="text" id="title-input" placeholder="Title" required>
<div class="sub-inputs">
<input type="datetime-local" id="time-input" required>
<label for="notification-service-selection-toggle" id="notification-service-selection-button">Notification Services</label>
</div>
<div class="notification-service-selection"></div>
<div class="repeat-options sub-inputs">
<button type="button" id="normal-button" data-selected="true">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
</g>
</svg>
Normal
</button>
<button type="button" id="repeat-button" data-selected="false">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
</g>
</svg>
Repeated
</button>
<button type="button" id="weekday-button" data-selected="false">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 507.506 507.506" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M163.865 436.934a54.228 54.228 0 0 1-38.4-15.915L9.369 304.966c-12.492-12.496-12.492-32.752 0-45.248 12.496-12.492 32.752-12.492 45.248 0l109.248 109.248L452.889 79.942c12.496-12.492 32.752-12.492 45.248 0 12.492 12.496 12.492 32.752 0 45.248L202.265 421.019a54.228 54.228 0 0 1-38.4 15.915z"></path>
</g>
</svg>
Week Days
</button>
</div>
<div class="repeat-bar hidden">
<p>Repeat every </p>
<input type="number" id="repeat-interval" placeholder="interval" min="1" step="1" oninput="validity.valid || (value='');">
<select id="repeat-quantity">
<option value="minutes">Minute(s)</option>
<option value="hours">Hour(s)</option>
<option value="days" selected>Day(s)</option>
<option value="weeks">Week(s)</option>
<option value="months">Month(s)</option>
<option value="years">Year(s)</option>
</select>
</div>
<div class="weekday-bar hidden">
<p>Mo</p>
<input type="checkbox">
<p>Tu</p>
<input type="checkbox">
<p>We</p>
<input type="checkbox">
<p>Thu</p>
<input type="checkbox">
<p>Fr</p>
<input type="checkbox">
<p>Sa</p>
<input type="checkbox">
<p>Su</p>
<input type="checkbox">
</div>
<textarea id="text-input" cols="30" rows="10" placeholder="Text (optional)"></textarea>
<div class="options">
<button type="button" id="close-info">Cancel</button>
<button type="button" id="test-reminder">
<div>Test</div>
<div>Sent</div>
</button>
<button type="button" id="delete-info">Delete</button>
<button type="submit">Add</button>
</div>
</form>
</div>
</div>
<h2>Contact and Donation</h2>
<div class="contact-list">
<a href="https://ko-fi.com/casvt">Donate to MIND</a>
<a href="https://casvt.github.io/MIND/">Documentation</a>
<a href="https://github.com/Casvt/MIND/issues">Report an issue</a>
<a href="https://discord.gg/nMNdgG7vsE">Discord server</a>
<div id="notification">
<h2>Notification Services</h2>
<p>Setup your notification providers here</p>
<div class="table-container">
<label
id="add-service-button"
title="Toggle adding notification service"
for="service-list-toggle"
>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
</g>
</g>
</svg>
</label>
<div class="overflow-container">
<input type="checkbox" id="service-list-toggle" class="hidden">
<input type="checkbox" id="add-service-toggle" class="hidden">
<table>
<thead>
<tr>
<th class="title-column">Title</th>
<th class="url-column">Apprise URL</th>
<th title="Actions" aria-label="Actions" class="action-column">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 24 24" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<path d="M11.24,24a2.262,2.262,0,0,1-.948-.212,2.18,2.18,0,0,1-1.2-2.622L10.653,16H6.975A3,3,0,0,1,4.1,12.131l3.024-10A2.983,2.983,0,0,1,10,0h3.693a2.6,2.6,0,0,1,2.433,3.511L14.443,8H17a3,3,0,0,1,2.483,4.684l-6.4,10.3A2.2,2.2,0,0,1,11.24,24ZM10,2a1,1,0,0,0-.958.71l-3.024,10A1,1,0,0,0,6.975,14H12a1,1,0,0,1,.957,1.29L11.01,21.732a.183.183,0,0,0,.121.241A.188.188,0,0,0,11.4,21.9l6.4-10.3a1,1,0,0,0,.078-1.063A.979.979,0,0,0,17,10H13a1,1,0,0,1-.937-1.351l2.19-5.84A.6.6,0,0,0,13.693,2Z"></path>
</g>
</svg>
</th>
</tr>
</thead>
<tbody id="services-list">
</tbody>
</table>
<div id="add-service-container">
<div id="service-list">
</div>
<form id="add-service-window"></form>
</div>
</div>
</div>
</div>
<div id="settings">
<h2>Settings</h2>
<table id="settings-table">
<tr>
<td><label for="locale-input">Locale</label></td>
<td>
<select id="locale-input">
<option value="ar-SA">ar-SA</option>
<option value="bn-BD">bn-BD</option>
<option value="bn-IN">bn-IN</option>
<option value="cs-CZ">cs-CZ</option>
<option value="da-DK">da-DK</option>
<option value="de-AT">de-AT</option>
<option value="de-CH">de-CH</option>
<option value="de-DE">de-DE</option>
<option value="el-GR">el-GR</option>
<option value="en-AU">en-AU</option>
<option value="en-CA">en-CA</option>
<option value="en-GB" selected>en-GB</option>
<option value="en-IE">en-IE</option>
<option value="en-IN">en-IN</option>
<option value="en-NZ">en-NZ</option>
<option value="en-US">en-US</option>
<option value="en-ZA">en-ZA</option>
<option value="es-AR">es-AR</option>
<option value="es-CL">es-CL</option>
<option value="es-CO">es-CO</option>
<option value="es-ES">es-ES</option>
<option value="es-MX">es-MX</option>
<option value="es-US">es-US</option>
<option value="fi-FI">fi-FI</option>
<option value="fr-BE">fr-BE</option>
<option value="fr-CA">fr-CA</option>
<option value="fr-CH">fr-CH</option>
<option value="fr-FR">fr-FR</option>
<option value="he-IL">he-IL</option>
<option value="hi-IN">hi-IN</option>
<option value="hu-HU">hu-HU</option>
<option value="id-ID">id-ID</option>
<option value="it-CH">it-CH</option>
<option value="it-IT">it-IT</option>
<option value="ja-JP">ja-JP</option>
<option value="ko-KR">ko-KR</option>
<option value="nl-BE">nl-BE</option>
<option value="nl-NL">nl-NL</option>
<option value="no-NO">no-NO</option>
<option value="pl-PL">pl-PL</option>
<option value="pt-BR">pt-BR</option>
<option value="pt-PT">pt-PT</option>
<option value="ro-RO">ro-RO</option>
<option value="ru-RU">ru-RU</option>
<option value="sk-SK">sk-SK</option>
<option value="sv-SE">sv-SE</option>
<option value="ta-IN">ta-IN</option>
<option value="ta-LK">ta-LK</option>
<option value="th-TH">th-TH</option>
<option value="tr-TR">tr-TR</option>
<option value="zh-CN">zh-CN</option>
<option value="zh-HK">zh-HK</option>
<option value="zh-TW">zh-TW</option>
</select>
</td>
</tr>
<tr>
<td><label for="default-service-input">Default Notification Service</label></td>
<td><select id="default-service-input"></select></td>
</tr>
<tr>
<td><label for="password-input">Change Password</label></td>
<td>
<form id="change-password-form">
<input type="password" id="password-input" autocomplete="new-password" required>
<button type="submit">Change</button>
</form>
</td>
</tr>
<tr>
<td><label for="delete-account-button">Delete Account</label></td>
<td><button id="delete-account-button">Delete Account</button></td>
</tr>
</table>
<h2>Contact and Donation</h2>
<div class="contact-list">
<a href="https://ko-fi.com/casvt">Donate to MIND</a>
<a href="https://casvt.github.io/MIND/">Documentation</a>
<a href="https://github.com/Casvt/MIND/issues">Report an issue</a>
<a href="https://discord.gg/nMNdgG7vsE">Discord server</a>
</div>
</div>
</div>
</main>