Refactored and refined login and admin UI

This commit is contained in:
CasVT
2025-05-17 20:01:19 +02:00
parent 39847f18cd
commit 77a2a67232
20 changed files with 2431 additions and 1291 deletions

10
.vscode/settings.json vendored
View File

@@ -1,7 +1,15 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 4,
"editor.linkedEditing": true,
"[html]": {
"editor.rulers": [120],
},
"html.format.wrapLineLength": 120,
"html.format.wrapAttributes": "aligned-multiple",
"html.format.templating": true,
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.autopep8",

2
.vscode/tasks.json vendored
View File

@@ -2,7 +2,7 @@
"version": "2.0.0",
"tasks": [
{
"label": "Format All",
"label": "Python: Format All",
"type": "shell",
"command": "python3 -m isort .; python3 -m autopep8 --in-place -r .",
"windows": {

View File

@@ -1,160 +1,108 @@
main {
position: relative;
}
main a {
color: var(--color-light);
}
.action-buttons {
--spacing: .5rem;
position: absolute;
margin: var(--spacing);
inset: 0 0 auto 0;
height: var(--nav-width);
display: flex;
justify-content: center;
align-items: center;
gap: calc(var(--spacing) * 3);
flex-wrap: wrap;
gap: 1rem;
padding: var(--spacing);
border-radius: 4px;
background-color: var(--color-gray);
padding: 1rem .5rem;
& > button {
width: 10rem;
display: flex;
justify-content: center;
align-items: center;
gap: .75rem;
padding: .5rem;
border-radius: 4px;
background-color: var(--color-gray);
color: var(--color-light);
transition:
background-color 150ms ease-in-out,
box-shadow 150ms ease-in-out;
&:hover {
background-color: var(--color-light-gray);
box-shadow: var(--default-shadow);
}
&.submit-error {
background-color: var(--color-error);
}
& > svg {
height: 1.8rem;
width: 2rem;
}
}
}
.action-buttons > button {
height: 100%;
#grid-container {
--grid-spacing: 1.25rem;
display: flex;
justify-content: center;
align-items: center;
width: min(100%, 80rem + 3 * var(--grid-spacing));
margin-inline: auto;
display: grid;
grid-template: auto auto auto auto / 1fr 1fr;
gap: var(--grid-spacing);
padding: var(--grid-spacing);
padding-bottom: 3rem;
}
#grid-container > section {
max-height: 40rem;
min-width: 0;
padding: .5rem;
border-radius: 4px;
border: 3px solid var(--color-gray);
background-color: var(--color-dark);
color: var(--color-light);
transition: background-color .1s ease-in-out;
}
.action-buttons > button:hover {
background-color: var(--color-gray);
}
.action-buttons > button > svg {
height: 1.8rem;
width: 2rem;
}
.form-container {
height: calc(100vh - var(--header-height));
overflow-y: auto;
padding: .5rem;
padding-top: var(--nav-width);
}
#settings-form,
#hosting-form {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
box-shadow: 0px 0px 8px 3px rgba(0 0 0 / 0.8);
}
h2 {
width: 100%;
padding: .4rem .75rem;
border-bottom: 1px solid var(--color-gray);
padding: 1rem 1rem .25rem 1rem;
font-size: clamp(1rem, 10vw, 2rem);
}
.settings-table-container,
.user-table-container {
width: 100%;
overflow-x: auto;
.collaps-table {
--min-width: 18rem;
--max-width: 40rem;
--header-width: 30%;
--min-data-width: 15rem;
}
#power-section > .table-container {
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
}
.settings-table {
--max-width: 55rem;
width: 100%;
max-width: var(--max-width);
min-width: 20rem;
border-spacing: 0px;
border: none;
tr:has(:where(input, select).changed) label {
color: var(--color-changed);
}
.settings-table td {
--middle-spacing: .75rem;
padding-bottom: 1rem;
vertical-align: top;
:where(input, select).changed,
div.input-style:has(input.changed) {
border-color: var(--color-changed);
}
.settings-table td:first-child {
width: 50%;
padding-right: var(--middle-spacing);
padding-top: .55rem;
text-align: right;
#backup-settings-section td:has(div.checked-input-container) {
height: 6.6rem;
}
.settings-table td:nth-child(2) {
min-width: calc(var(--max-width) * 0.5);
padding-left: var(--middle-spacing);
}
.add-item-container {
margin: 1rem;
.settings-table td p {
color: var(--color-light-gray);
font-size: .9rem;
}
.settings-table td > p {
margin-top: .25rem;
}
.number-input {
width: fit-content;
display: flex;
align-items: center;
border: 2px solid var(--color-gray);
border-radius: 4px;
box-shadow: var(--default-shadow);
}
.number-input > input {
width: auto;
border: none;
box-shadow: none;
text-align: right;
}
.number-input > * {
padding: .5rem 1rem;
}
.number-input > p {
padding-left: 0;
}
.settings-table select {
width: auto;
}
.add-user-container,
.database-container {
margin-top: 2rem;
margin-bottom: 1rem;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
@@ -162,140 +110,103 @@ h2 {
flex-wrap: wrap;
}
#download-logs-button,
#save-hosting-button,
#add-user-button,
#upload-db-button,
#restart-button,
#shutdown-button {
width: min(15rem, 100%);
.entries-table {
margin-inline: auto;
margin-bottom: 1rem;
padding: .5rem 1rem;
border-radius: 4px;
background-color: var(--color-gray);
box-shadow: var(--default-shadow);
width: clamp(22rem, 100%, 40rem);
}
.database-container:has(#download-logs-button) {
margin: 0;
}
#save-hosting-button {
align-self: center;
}
#add-user-button {
height: 2rem;
}
#add-user-button > svg {
aspect-ratio: 1/1;
height: 1rem;
width: min-content;
}
#user-table,
#backup-table {
min-width: 25rem;
border-spacing: 0px;
}
:where(#user-table, #backup-table) :where(th, td) {
height: 2.65rem;
padding: .25rem .5rem;
text-align: left;
}
:where(#user-table, #backup-table) tr td {
border-top: 1px solid var(--color-gray);
}
:where(#user-table, #backup-table) :where(th, td):first-child {
width: 10rem;
padding-left: 2rem;
}
:where(#user-table, #backup-table) :where(th, td):nth-child(2) {
width: 15rem;
}
:where(#user-table, #backup-table) :where(th, td):last-child {
width: 5.75rem;
display: flex;
align-items: center;
gap: 1rem;
padding-right: 2rem;
}
:where(#user-table, #backup-table) button {
height: 1.25rem;
}
:where(#user-table, #backup-table) svg {
aspect-ratio: 1/1;
height: 100%;
width: auto;
}
#user-table input {
#user-table td:first-child {
width: 100%;
padding: .25rem;
}
#upload-database-form {
#user-table td:last-child {
width: 6rem;
}
#backup-table {
width: clamp(30rem, 100%, 40rem);
}
#backup-table td:first-child {
width: 100%;
}
#backup-table td:nth-child(2) {
text-wrap-mode: nowrap;
}
#add-user-form,
#edit-user-form {
width: 16rem;
height: 8rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
margin-block: 2rem 1rem;
}
#hosting-form > p,
#upload-database-form > p {
max-width: 50rem;
margin-inline: auto;
#edit-user-form:has(div.hidden) {
height: unset;
}
dialog span {
font-weight: bold;
}
dialog:where(#upload-db-dialog, #import-db-dialog) {
--max-height: 25rem;
}
:where(#upload-db-dialog, #import-db-dialog) .dialog-content > p {
padding-inline: 1rem;
text-align: center;
word-wrap: break-word;
}
#backup-table :where(th, td):first-child {
width: 20rem;
#upload-db-form tr:first-child td:last-child {
height: 6.1rem;
}
@media (max-width: 40rem) {
#reset-settings-dialog .dialog-content {
height: calc(100% - 2 * 3.2rem);
justify-content: flex-start;
overflow-y: auto;
}
#reset-settings-form {
width: 100%;
}
#reset-table {
max-width: 30rem;
}
@media (max-width: 24rem) {
.action-buttons button {
width: 100%;
&:last-of-type {
flex-direction: row-reverse;
}
}
}
@media (max-width: 29rem) {
.entries-table {
--inline-padding: 0;
}
}
@media (max-width: 52.25rem) {
#grid-container {
grid-template: repeat(8, auto) / 1fr;
padding-bottom: var(--grid-spacing);
}
h2 {
text-align: center;
padding-inline: 0;
}
.settings-table-container,
.user-table-container {
align-items: flex-start;
}
.settings-table tbody {
display: flex;
flex-direction: column;
}
.settings-table tr {
display: inline-flex;
flex-direction: column;
padding-left: 1rem;
}
.settings-table td {
width: 100%;
}
.settings-table td:first-child {
width: 100%;
text-align: left;
}
.settings-table td:nth-child(2) {
min-width: 0;
}
}

View File

@@ -2,28 +2,41 @@
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
font-size: 1rem;
}
:root {
--color-light: #ffffff;
--color-light-gray: #6b6b6b;
--color-gray: #3c3c3c;
--color-dark: #1b1b1b;
--color-mid-gray: #5c5c5c;
--color-gray: #232323;
--color-dark: #111111;
--color-changed: #b87000;
--color-error: #db5461;
--color-dim-error: #bc4551;
--color-success: #54db68;
--header-height: 4.5rem;
--nav-width: 4rem;
--scrollbar-width: 12px;
--rem-clamp: clamp(.5rem, 2vw, 1rem);
--default-shadow: 0 1px 2px 0 rgb(0 0 0 / 60%), 0 2px 6px 2px rgb(0 0 0 / 30%);
--default-shadow:
0 1px 2px 0 rgb(0 0 0 / 60%),
0 2px 6px 2px rgb(0 0 0 / 30%);
}
/* */
/* Default properties */
/* */
dialog {
margin: auto;
}
img {
width: 100%;
}
@@ -31,9 +44,6 @@ img {
button,
label {
border: 0;
border-radius: 4px;
background-color: var(--color-dark);
color: var(--color-light);
}
button:hover,
@@ -41,22 +51,6 @@ label:hover {
cursor: pointer;
}
input:not([type="checkbox"]),
select,
textarea,
.as-button {
border: 2px solid var(--color-gray);
border-radius: 4px;
padding: .6rem;
outline: 0;
background-color: var(--color-dark);
color: var(--color-light);
box-shadow: var(--default-shadow);
font-size: 1rem;
}
input::placeholder,
textarea::placeholder {
color: var(--color-gray);
@@ -66,14 +60,21 @@ input[type="datetime-local"] {
color-scheme: dark;
}
svg path,
svg rect {
svg :where(path, rect) {
fill: var(--color-light);
}
a {
color: var(--color-light);
}
table {
border-spacing: 0px;
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
width: var(--scrollbar-width);
height: var(--scrollbar-width);
background-color: var(--color-dark);
}
@@ -120,11 +121,255 @@ svg rect {
font-size: 1rem !important;
}
button.spinning svg,
svg.spinning {
animation: spin-element 2.5s linear infinite forwards;
}
@keyframes spin-element {
from { transform: rotate(0deg) }
to { transform: rotate(360deg) }
}
:where(input, textarea, select, button, div).input-style {
height: 2.7rem;
width: 100%;
border: 2px solid var(--color-gray);
border-radius: 4px;
outline: 0;
background-color: var(--color-dark);
color: var(--color-light);
box-shadow: var(--default-shadow);
transition: background-color 150ms ease-in-out;
font-size: 1rem;
}
:where(input, textarea, select, button, div).input-style:focus-within {
border-color: var(--color-light-gray);
}
:where(input, textarea, select, button).input-style,
div.input-style > :where(input, p) {
padding: .5rem;
}
button:has(> svg).input-style {
display: flex;
justify-content: center;
align-items: center;
}
button.input-style > svg {
height: 1.3rem;
width: auto;
}
div.input-style {
width: fit-content;
display: flex;
align-items: center;
& > input {
width: 100%;
height: 100%;
border: 0;
outline: 0;
background-color: var(--color-dark);
color: var(--color-light);
}
}
button.input-style:hover {
background-color: var(--color-gray);
}
div.input-style > input[type="number"],
input[type="number"].input-style {
max-width: 6rem;
}
div.input-style > input[type="file"],
input[type="file"].input-style {
max-width: 20rem;
}
div.input-style > :where(input[type="text"], select),
:where(input[type="text"], select).input-style {
max-width: 16rem;
}
button.input-style {
max-width: 16rem;
}
:where(input, textarea, select, button).long-input-style,
div.long-input-style > input {
max-width: 25rem;
}
.checked-input-container {
position: relative;
height: 100%;
max-height: 2.7rem;
width: 100%;
max-width: 16rem;
transition: max-height 50ms ease-in-out,
border-color 50ms ease-in-out;
}
.checked-input-container.error-input-container {
max-height: 4.1rem;
}
.checked-input-container.error-input-container input {
border-color: var(--color-error);
}
.checked-input-container :where(input, textarea) {
position: relative;
z-index: 1;
}
.checked-input-container p {
position: absolute;
bottom: 0;
height: 1.9rem;
width: 100%;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border: 2px solid var(--color-error);
padding: .6rem .65rem .3rem .65rem;
background-color: var(--color-dim-error);
color: var(--color-light);
font-size: .7rem;
}
.table-container {
width: 100%;
overflow: auto;
}
.entries-table {
--inline-padding: 1.5rem;
position: relative;
padding-inline: var(--inline-padding);
}
.entries-table::before {
content: "";
position: absolute;
inset: 0 auto 0 var(--inline-padding);
width: 1.4rem;
background: linear-gradient(90deg, var(--color-dark), transparent);
}
.entries-table::after {
content: "";
position: absolute;
inset: 0 var(--inline-padding) 0 auto;
width: 1.4rem;
background: linear-gradient(270deg, var(--color-dark), transparent);
}
.entries-table > tbody > tr {
transition: background-color 200ms ease-in-out;
&:hover {
background-color: var(--color-gray);
}
}
.entries-table :where(th, td) {
height: 2.65rem;
padding: .25rem .5rem;
text-align: left;
}
.entries-table td {
border-top: 1px solid var(--color-gray);
}
.entries-table :where(th, td):first-child {
padding-left: 2rem;
}
.entries-table :where(th, td):last-child {
padding-right: 2rem;
}
.entries-table td:last-child:has(button) {
display: flex;
align-items: center;
gap: .75rem;
}
.entries-table td:last-child button {
height: 1.25rem;
background-color: transparent;
}
.entries-table svg {
aspect-ratio: 1/1;
height: 100%;
width: auto;
}
.collaps-table {
--min-width: 20rem;
--max-width: 55rem;
--header-width: 50%;
--min-data-width: 25rem;
width: clamp(var(--min-width), 100%, var(--max-width));
padding-inline: 1rem;
}
.collaps-table tr:first-of-type :where(th, td) {
padding-top: 1rem;
}
.collaps-table :where(th, td) {
--middle-spacing: .75rem;
padding-bottom: 1rem;
vertical-align: top;
&:first-child {
width: var(--header-width);
padding-right: var(--middle-spacing);
text-align: right;
}
&:last-child {
min-width: var(--min-data-width);
padding-left: var(--middle-spacing);
}
& > p {
margin-top: .25rem;
color: var(--color-light-gray);
font-size: .9rem;
}
}
/* */
/* General styling */
/* */
body {
height: 100vh;
height: 100dvh;
overflow-x: hidden;
background-color: var(--color-dark);
@@ -141,44 +386,23 @@ noscript {
padding: 1rem;
background-color: var(--color-error);
color: var(--color-light);
text-align: center;
}
/* */
/* Header */
/* */
header {
width: 100%;
height: var(--header-height);
display: flex;
align-items: center;
padding: 1rem;
box-shadow: var(--default-shadow);
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
background-color: var(--color-gray);
}
header > div {
height: 100%;
transform: translateX(-2.6rem);
display: flex;
align-items: center;
gap: 1rem;
transition: transform .3s ease-in-out;
}
#toggle-nav {
--height: 1.5rem;
height: var(--height);
background-color: transparent;
}
#toggle-nav svg {
height: var(--height);
width: var(--height);
box-shadow: var(--default-shadow);
}
header img {
@@ -186,152 +410,207 @@ header img {
width: fit-content;
}
/* */
/* Nav */
/* */
.nav-divider {
position: relative;
height: calc(100% - var(--header-height));
dialog {
--ani-duration: 100ms;
--max-height: 23rem;
width: min(100%, 40rem);
height: min(100%, var(--max-height));
border-radius: 6px;
border: 4px solid var(--color-gray);
background-color: var(--color-dark);
color: var(--color-light);
box-shadow: 0px 0px 20px 5px rgba(0 0 0 / 0.8);
display: flex;
padding-block: var(--rem-clamp);
animation: vanish-dialog var(--ani-duration);
}
body:has(#nav-switch:checked) .nav-divider > nav {
left: var(--rem-clamp);
dialog[open] {
animation: appear-dialog var(--ani-duration);
}
body:has(#nav-switch:checked) .nav-divider > .window-container {
margin-left: calc(var(--nav-width) + var(--rem-clamp));
dialog::backdrop {
backdrop-filter: blur(3px);
}
nav {
--padding: .5rem;
z-index: 1;
position: absolute;
left: var(--rem-clamp);
height: calc(100% - (2 * var(--rem-clamp)));
width: var(--nav-width);
dialog[open]::backdrop {
animation: appear-dialog-bg var(--ani-duration);
}
@keyframes appear-dialog {
from {
opacity: 0;
translate: 0 -5%;
}
to {
opacity: 1;
translate: 0 0;
}
}
@keyframes vanish-dialog {
from {
display: block;
opacity: 1;
translate: 0 0;
}
to {
display: none;
opacity: 0;
translate: 0 -5%;
}
}
@keyframes appear-dialog-bg {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.dialog-container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: var(--padding);
overflow-y: auto;
padding: var(--padding);
border-radius: 4px;
background-color: var(--color-gray);
transition: left .3s ease-in-out;
}
nav > div {
width: 100%;
.dialog-header, .dialog-footer {
height: 3.2rem;
display: flex;
align-items: center;
gap: 1rem;
padding: .4rem .75rem;
&.dialog-header {
justify-content: flex-start;
border-bottom: 2px solid var(--color-gray);
}
&.dialog-footer {
justify-content: flex-end;
border-top: 2px solid var(--color-gray);
}
}
.dialog-header h2 {
padding: 0;
border-bottom: 0;
}
.dialog-footer > button,
.confirm-container > button {
width: 5rem;
height: 2.3rem;
padding: 0;
&:first-of-type:where(:hover, :focus-within) {
background-color: var(--color-dim-error);
}
&:last-of-type:where(:hover, :focus-within) {
background-color: var(--color-success);
}
}
.dialog-content {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: var(--padding);
}
nav > div > button {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: .5rem;
border: 0;
border-radius: 4px;
background-color: var(--color-dark);
color: var(--color-light);
transition: background-color .1s ease-in-out;
gap: 1rem;
}
nav > div > button:hover {
background-color: var(--color-gray);
}
nav > div > button svg {
height: 1.8rem;
width: 2rem;
}
/* */
/* Window management */
/* */
.window-container {
margin-left: calc(4rem + var(--rem-clamp));
width: 100%;
.confirm-container {
display: flex;
overflow: hidden;
transition: margin-left .3s ease-in-out;
flex-direction: row-reverse;
gap: 1rem;
}
.window-container > :where(#home, .extra-window-container) {
width: 100%;
flex: 0 0 auto;
translate: 0 0;
transition: translate .5s ease-in-out;
}
.window-container.inter-window-ani > :where(#home, .extra-window-container) {
transition: translate .5s ease-in-out,
transform .5s ease-in-out;
}
.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);
}
.extra-window-container > div > h2 {
text-align: center;
font-size: clamp(1.3rem, 5vw, 2rem);
margin-bottom: 2rem;
}
.extra-window-container > div > h2:not(:first-of-type) {
margin-top: 1.5rem;
}
.extra-window-container > div > p {
text-align: center;
}
@media (max-width: 543px) {
.window-container {
margin-left: 0;
.confirm-container > button {
width: 6rem;
&:first-of-type:where(:hover, :focus-within) {
background-color: var(--color-success);
}
nav {
left: -100%;
&:last-of-type:where(:hover, :focus-within) {
background-color: var(--color-dim-error);
}
}
@media (prefers-reduced-motion) {
* {
animation-duration: 0ms !important;
transition-duration: 0ms !important;
}
}
@media (max-width: 29rem) {
.collaps-table tbody {
display: flex;
flex-direction: column;
}
.collaps-table tr {
display: inline-flex;
flex-direction: column;
padding-left: .5rem;
}
.collaps-table tr:first-of-type > td {
padding-top: 0rem;
}
.collaps-table :where(th, td) {
width: 100%;
&:first-child {
width: 100%;
text-align: left;
}
&:last-child {
min-width: unset;
}
}
dialog {
margin: 0;
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
border-radius: 0;
border: 0;
}
@keyframes appear-dialog {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes vanish-dialog {
from {
display: block;
opacity: 1;
}
to {
display: none;
opacity: 0;
}
}
}

View File

@@ -126,6 +126,7 @@ body:has(#wide-toggle:checked) .tab-container > div {
border-radius: 4px;
padding: .75rem;
background-color: var(--color);
color: var(--color-light);
}
button.entry.fit {

View File

@@ -1,65 +1,46 @@
main {
height: calc(100vh - var(--header-height));
overflow-y: hidden;
}
main:has(#form-switch:checked) .form-container {
transform: translateY(-100%);
}
.form-container {
height: inherit;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
transition: transform .25s ease-in-out;
}
form {
width: 30rem;
margin-inline: auto;
height: calc(100% - var(--header-height));
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
}
.login-container {
position: relative;
width: min(40rem, 90%);
height: 22rem;
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
overflow: hidden;
border-radius: 4px;
padding: 1rem;
background-color: var(--color-gray);
color: var(--color-light);
box-shadow: 0px 0px 20px 3px rgba(0 0 0 / 0.25);
}
form {
height: inherit;
flex: 1 0 auto;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 2rem;
transition: translate 100ms ease-in-out;
}
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 {
width: min(100%, 20rem);
}
#username-error-container p,
#password-error-container p {
width: min(100%, 19rem);
}
.switch-button {
border: 0;
background-color: transparent;
@@ -69,12 +50,53 @@ form input {
text-align: center;
}
.switch-button:hover {
cursor: pointer;
}
button[type="submit"] {
width: 7rem;
padding: .5rem 1rem;
border-radius: 4px;
background-color: var(--color-mid-gray);
color: var(--color-light);
font-size: 1.1rem;
transition:
background-color 150ms ease-in-out,
box-shadow 150ms ease-in-out;
}
button[type="submit"]:hover {
background-color: var(--color-light-gray);
box-shadow: var(--default-shadow);
}
#form-cover {
position: absolute;
left: 0;
height: 100%;
width: 50%;
z-index: 2;
display: flex;
padding: 3rem;
border-radius: 4px;
background-color: var(--color-gray);
box-shadow: 0px 0px 20px 3px rgba(0 0 0 / 0.25);
transition: left 100ms ease-in-out;
}
main:has(#form-switch:checked) #form-cover {
left: 50%;
}
@media (max-width: 594px) {
#form-cover {
display: none;
}
main:has(#form-switch:checked) form {
translate: 0 -100%;
}
}

View File

@@ -0,0 +1,175 @@
/* */
/* Nav collapse button */
/* */
header > div {
height: 100%;
transform: translateX(-2.6rem);
display: flex;
align-items: center;
gap: 1rem;
transition: transform .3s ease-in-out;
}
#toggle-nav {
--height: 1.5rem;
height: var(--height);
background-color: transparent;
}
#toggle-nav svg {
height: var(--height);
width: var(--height);
}
/* */
/* Nav */
/* */
.nav-divider {
position: relative;
height: calc(100% - var(--header-height));
display: flex;
padding-block: var(--rem-clamp);
}
body:has(#nav-switch:checked) .nav-divider > nav {
left: var(--rem-clamp);
}
body:has(#nav-switch:checked) .nav-divider > .window-container {
margin-left: calc(var(--nav-width) + var(--rem-clamp));
}
nav {
--padding: .5rem;
z-index: 1;
position: absolute;
left: var(--rem-clamp);
height: calc(100% - (2 * var(--rem-clamp)));
width: var(--nav-width);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: var(--padding);
overflow-y: auto;
padding: var(--padding);
border-radius: 4px;
background-color: var(--color-gray);
transition: left .3s ease-in-out;
}
nav > div {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--padding);
}
nav > div > button {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: .5rem;
border: 0;
border-radius: 4px;
background-color: var(--color-dark);
color: var(--color-light);
transition: background-color .1s ease-in-out;
}
nav > div > button:hover {
background-color: var(--color-gray);
}
nav > div > button svg {
height: 1.8rem;
width: 2rem;
}
/* */
/* Window management */
/* */
.window-container {
margin-left: calc(4rem + var(--rem-clamp));
width: 100%;
display: flex;
overflow: hidden;
transition: margin-left .3s ease-in-out;
}
.window-container > :where(#home, .extra-window-container) {
width: 100%;
flex: 0 0 auto;
translate: 0 0;
transition: translate .5s ease-in-out;
}
.window-container.inter-window-ani > :where(#home, .extra-window-container) {
transition: translate .5s ease-in-out,
transform .5s ease-in-out;
}
.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);
}
.extra-window-container > div > h2 {
text-align: center;
font-size: clamp(1.3rem, 5vw, 2rem);
margin-bottom: 2rem;
}
.extra-window-container > div > h2:not(:first-of-type) {
margin-top: 1.5rem;
}
.extra-window-container > div > p {
text-align: center;
}
@media (max-width: 543px) {
.window-container {
margin-left: 0;
}
nav {
left: -100%;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,126 +1,270 @@
// The duration of the animation set for the window translation
const window_ani_ms = 500;
//
// region Definitions
//
const constants = {
/**
* The duration of the animation set for the window translation
*/
windowAnimationDuration: 500
}
const Types = {
'reminder': document.getElementById('reminder-tab'),
'static_reminder': document.getElementById('static-reminder-tab'),
'template': document.getElementById('template-tab')
};
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>',
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512" height="512"><g><path d="M210.731,386.603c24.986,25.002,65.508,25.015,90.51,0.029c0.01-0.01,0.019-0.019,0.029-0.029l68.501-68.501 c7.902-8.739,7.223-22.23-1.516-30.132c-8.137-7.357-20.527-7.344-28.649,0.03l-62.421,62.443l0.149-329.109 C277.333,9.551,267.782,0,256,0l0,0c-11.782,0-21.333,9.551-21.333,21.333l-0.192,328.704L172.395,288 c-8.336-8.33-21.846-8.325-30.176,0.011c-8.33,8.336-8.325,21.846,0.011,30.176L210.731,386.603z"/><path d="M490.667,341.333L490.667,341.333c-11.782,0-21.333,9.551-21.333,21.333V448c0,11.782-9.551,21.333-21.333,21.333H64 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0C9.551,341.333,0,350.885,0,362.667V448 c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512,350.885,502.449,341.333,490.667,341.333z"/></g></svg>',
upload: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512.008 512.008" style="enable-background:new 0 0 512.008 512.008;" xml:space="preserve" width="512" height="512"><g><path d="M172.399,117.448l62.421-62.443l-0.149,329.344c0,11.782,9.551,21.333,21.333,21.333l0,0 c11.782,0,21.333-9.551,21.333-21.333l0.149-328.981l62.123,62.144c8.475,8.185,21.98,7.951,30.165-0.524 c7.985-8.267,7.985-21.374,0-29.641L301.273,18.76c-24.986-25.002-65.508-25.015-90.51-0.029c-0.01,0.01-0.019,0.019-0.029,0.029 l-68.501,68.523c-8.185,8.475-7.951,21.98,0.524,30.165C151.024,125.433,164.131,125.433,172.399,117.448z"/><path d="M490.671,341.341L490.671,341.341c-11.782,0-21.333,9.551-21.333,21.333v85.333c0,11.782-9.551,21.333-21.333,21.333h-384 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0c-11.782,0-21.333,9.551-21.333,21.333 v85.333c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512.004,350.892,502.453,341.341,490.671,341.341z"/></g></svg>',
loading: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m10.5 1.5c0-.828.672-1.5 1.5-1.5s1.5.672 1.5 1.5-.672 1.5-1.5 1.5-1.5-.672-1.5-1.5zm1.5 22.5c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm9-12c0 .828.672 1.5 1.5 1.5s1.5-.672 1.5-1.5-.672-1.5-1.5-1.5-1.5.672-1.5 1.5zm-21 0c0 .828.672 1.5 1.5 1.5s1.5-.672 1.5-1.5-.672-1.5-1.5-1.5-1.5.672-1.5 1.5zm17.293-7.577c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm3.779 3.798c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-.01 10.567c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-3.788 3.788c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-10.577-.01c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-3.75-3.779c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm-.01-10.596c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5zm3.779-3.769c.828 0 1.5-.672 1.5-1.5s-.672-1.5-1.5-1.5-1.5.672-1.5 1.5.672 1.5 1.5 1.5z"/></svg>'
}
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>',
'download': '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512" height="512"><g><path d="M210.731,386.603c24.986,25.002,65.508,25.015,90.51,0.029c0.01-0.01,0.019-0.019,0.029-0.029l68.501-68.501 c7.902-8.739,7.223-22.23-1.516-30.132c-8.137-7.357-20.527-7.344-28.649,0.03l-62.421,62.443l0.149-329.109 C277.333,9.551,267.782,0,256,0l0,0c-11.782,0-21.333,9.551-21.333,21.333l-0.192,328.704L172.395,288 c-8.336-8.33-21.846-8.325-30.176,0.011c-8.33,8.336-8.325,21.846,0.011,30.176L210.731,386.603z"/><path d="M490.667,341.333L490.667,341.333c-11.782,0-21.333,9.551-21.333,21.333V448c0,11.782-9.551,21.333-21.333,21.333H64 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0C9.551,341.333,0,350.885,0,362.667V448 c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512,350.885,502.449,341.333,490.667,341.333z"/></g></svg>',
'upload': '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512.008 512.008" style="enable-background:new 0 0 512.008 512.008;" xml:space="preserve" width="512" height="512"><g><path d="M172.399,117.448l62.421-62.443l-0.149,329.344c0,11.782,9.551,21.333,21.333,21.333l0,0 c11.782,0,21.333-9.551,21.333-21.333l0.149-328.981l62.123,62.144c8.475,8.185,21.98,7.951,30.165-0.524 c7.985-8.267,7.985-21.374,0-29.641L301.273,18.76c-24.986-25.002-65.508-25.015-90.51-0.029c-0.01,0.01-0.019,0.019-0.029,0.029 l-68.501,68.523c-8.185,8.475-7.951,21.98,0.524,30.165C151.024,125.433,164.131,125.433,172.399,117.448z"/><path d="M490.671,341.341L490.671,341.341c-11.782,0-21.333,9.551-21.333,21.333v85.333c0,11.782-9.551,21.333-21.333,21.333h-384 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0c-11.782,0-21.333,9.551-21.333,21.333 v85.333c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512.004,350.892,502.453,341.341,490.671,341.341z"/></g></svg>'
};
const InfoClasses = [
'show-add-reminder', 'show-add-template', 'show-add-static-reminder',
'show-edit-reminder', 'show-edit-template', 'show-edit-static-reminder'
];
//
// region Helpers
//
function showWindow(id) {
const window_container = document.querySelector('.window-container');
if (id === "home") {
window_container.classList.remove(
'show-window',
'inter-window-ani'
);
} else {
const extra_window_container =
document.querySelector('.extra-window-container');
/**
* Hide and show elements.
*
* @param {Array<HTMLElement>} to_hide The elements to hide,
* by adding the `hidden` class.
*
* @param {Array<HTMLElement>?} to_show The elements to show,
* by removing the `hidden` class.
*/
function hide(to_hide, to_show) {
to_hide.forEach(el => el.classList.add('hidden'))
if (to_show !== null && to_show !== undefined)
to_show.forEach(el => el.classList.remove('hidden'))
}
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() {
fetch(`${url_prefix}/api/auth/logout?api_key=${api_key}`, {
'method': 'POST'
})
.then(response => {
setLocalStorage({'api_key': null});
window.location.href = `${url_prefix}/`;
});
};
//
// LocalStorage
//
const default_values = {
'api_key': null,
'locale': 'en-GB',
'default_service': null,
'sorting_reminders': 'time',
'sorting_static': 'title',
'sorting_templates': 'title'
};
function setupLocalStorage() {
if (!localStorage.getItem('MIND'))
localStorage.setItem('MIND', JSON.stringify(default_values));
const missing_keys = [
...Object.keys(default_values)
].filter(e =>
![...Object.keys(JSON.parse(localStorage.getItem('MIND')))].includes(e)
)
if (missing_keys.length) {
const storage = JSON.parse(localStorage.getItem('MIND'));
missing_keys.forEach(missing_key => {
storage[missing_key] = default_values[missing_key]
})
localStorage.setItem('MIND', JSON.stringify(storage));
};
return;
};
//
// region LocalStorage
//
const defaultValues = {
api_key: null,
locale: 'en-GB',
default_service: null,
sorting_reminders: 'time',
sorting_static: 'title',
sorting_templates: 'title',
allow_new_accounts_cache: true
}
/**
* Get the configuration stored in the local storage of the client (browser).
*
* @param {string | string[] | null | undefined} keys The keys to fetch, or all
* if null or undefined.
*
* @returns {Map<string, any>} The keys and their values.
*/
function getLocalStorage(keys) {
const storage = JSON.parse(localStorage.getItem('MIND'));
const result = {};
const storage = JSON.parse(localStorage.getItem('MIND'))
if (keys === undefined || keys === null)
return storage
const result = {}
if (typeof keys === 'string')
result[keys] = storage[keys];
result[keys] = storage[keys]
else if (typeof keys === 'object')
for (const key in keys)
result[key] = storage[key];
result[key] = storage[key]
return result;
};
return result
}
/**
* Update the configuration stored in the local storage of the client (browser).
* @param {Map<string, any>} keys_values The new values for the given keys.
*/
function setLocalStorage(keys_values) {
const storage = JSON.parse(localStorage.getItem('MIND'));
const storage = JSON.parse(localStorage.getItem('MIND'))
for (const [key, value] of Object.entries(keys_values))
storage[key] = value;
localStorage.setItem('MIND', JSON.stringify(storage));
return;
};
storage[key] = value
// code run on load
localStorage.setItem('MIND', JSON.stringify(storage))
}
setupLocalStorage();
/**
* Setup the configuration stored in the local storage of the client (browser)
* with default values.
*/
function setupLocalStorage() {
if (!localStorage.getItem('MIND'))
localStorage.setItem('MIND', JSON.stringify(defaultValues))
const url_prefix = document.getElementById('url_prefix').dataset.value;
const api_key = getLocalStorage('api_key')['api_key'];
if (api_key === null) {
window.location.href = `${url_prefix}/`;
};
const currentValues = getLocalStorage()
const cleanedVersion = {}
Object.keys(defaultValues).forEach(k => {
if (currentValues[k] === undefined)
cleanedVersion[k] = defaultValues[k]
else
cleanedVersion[k] = currentValues[k]
})
localStorage.setItem('MIND', JSON.stringify(cleanedVersion))
}
//
// region Fetching
//
/**
* Make a GET request to the API.
*
* @param {string} endpoint The endpoint to make the request to, without API URL
* prefix.
*
* @param {Map<string, any>} params The URL parameters to use in the request.
* API key doesn't need to be supplied.
*
* @param {bool} jsonReturn Whether to return the json response, or the raw
* response.
*
* @param {bool} checkAuth Check whether authentication failed and if so
* automatically redirect to login page.
*
* @returns The response.
*/
async function fetchAPI(endpoint, params={}, jsonReturn=true, checkAuth=true) {
let formattedParams = ''
if (apiKey !== null)
params.api_key = apiKey
if (Object.keys(params).length) {
formattedParams = '?' + Object.entries(params).map(p => p.join('=')).join('&')
}
return fetch(`${urlPrefix}/api${endpoint}${formattedParams}`)
.then(response => {
if (!response.ok)
return Promise.reject(response)
if (jsonReturn)
return response.json()
else
return response
})
.catch(response => {
if (checkAuth && response.status === 401) {
setLocalStorage({api_key: null})
window.location.href = `${urlPrefix}/`
} else {
return Promise.reject(response)
}
})
}
/**
* Make a POST, PUT or DELETE request to the API.
*
* @param {string} method The HTTP method to use.
*
* @param {string} endpoint The endpoint to make the request to, without API URL
* prefix.
*
* @param {Map<string, any>} params The URL parameters to use in the request.
* API key doesn't need to be supplied.
*
* @param {any} body The JSON to supply in the body of the request.
*
* @param {bool} jsonReturn Whether to return the json response, or the raw
* response.
*
* @param {bool} checkAuth Check whether authentication failed and if so
* automatically redirect to login page.
*
* @returns The response.
*/
async function sendAPI(
method,
endpoint,
params={},
body={},
jsonReturn=true,
checkAuth=true
) {
let formattedParams = ''
if (apiKey !== null)
params.api_key = apiKey
if (Object.keys(params).length) {
formattedParams = '?' + Object.entries(params).map(p => p.join('=')).join('&')
}
return fetch(`${urlPrefix}/api${endpoint}${formattedParams}`, {
'method': method,
'headers': {'Content-Type': 'application/json'},
'body': Object.entries(body).length !== 0 ? JSON.stringify(body) : ''
})
.then(response => {
if (!response.ok)
return Promise.reject(response)
if (jsonReturn)
return response.json()
else
return response
})
.catch(response => {
if (checkAuth && response.status === 401) {
setLocalStorage({api_key: null})
window.location.href = `${urlPrefix}/`
} else {
return Promise.reject(response)
}
})
}
/**
* Check the login status, and redirect to proper place if not already there.
*/
function checkLogin() {
if (apiKey === null)
window.location.href = `${urlPrefix}/`
fetchAPI("/auth/status")
.then(json => {
if (
json.result.admin
&& window.location.pathname !== `${urlPrefix}/admin`
)
window.location.href = `${urlPrefix}/admin`
else if (
!json.result.admin
&& window.location.pathname !== `${urlPrefix}/reminders`
)
window.location.href = `${urlPrefix}/reminders`
})
.catch(e => console.log(e))
}
/**
* Log out and redirect to login page.
*/
function logout() {
sendAPI("POST", "/auth/logout")
.then(response => {
setLocalStorage({'api_key': null})
window.location.href = `${urlPrefix}/`
})
}
//
// region On load
//
setupLocalStorage()
/**
* The URL prefix that the application is running on.
*/
const urlPrefix = document.getElementById('url_prefix').dataset.value
/**
* The API key that can be used to authenticate API requests. Not guaranteed to
* be valid.
*/
const apiKey = getLocalStorage('api_key')['api_key']

View File

@@ -22,11 +22,11 @@ const LibEls = {
//
function getSorting(type, key=false) {
let sorting_key;
if (type === Types.reminder)
if (type === reminderTypes.reminder)
sorting_key = 'sorting_reminders';
else if (type === Types.static_reminder)
else if (type === reminderTypes.static_reminder)
sorting_key = 'sorting_static';
else if (type === Types.template)
else if (type === reminderTypes.template)
sorting_key = 'sorting_templates';
if (key)
@@ -36,7 +36,7 @@ function getSorting(type, key=false) {
};
function getActiveTab() {
for (let t of Object.values(Types)) {
for (let t of Object.values(reminderTypes)) {
if (getComputedStyle(t).display === 'flex')
return t
};
@@ -63,7 +63,7 @@ function fillTable(table, results) {
title.innerText = r.title;
entry.appendChild(title);
if (table === Types.reminder) {
if (table === reminderTypes.reminder) {
const time = document.createElement('p');
let offset = new Date(r.time * 1000).getTimezoneOffset() * -60;
let d = new Date((r.time + offset) * 1000);
@@ -97,21 +97,21 @@ function fillLibrary(type=null) {
let tab_type = type || getActiveTab();
let url;
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`;
if (tab_type === reminderTypes.reminder)
url = `${urlPrefix}/api/reminders`;
else if (tab_type === reminderTypes.static_reminder)
url = `${urlPrefix}/api/staticreminders`;
else if (tab_type === reminderTypes.template)
url = `${urlPrefix}/api/templates`;
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}`;
url = `${url}/search?api_key=${apiKey}&sort_by=${sorting}&query=${query}`;
else
url = `${url}?api_key=${api_key}&sort_by=${sorting}`;
url = `${url}?api_key=${apiKey}&sort_by=${sorting}`;
fetch(url)
.then(response => {
@@ -121,7 +121,7 @@ function fillLibrary(type=null) {
.then(json => fillTable(tab_type, json.result))
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else
console.log(e);
});
@@ -148,8 +148,8 @@ function evaluateSizing() {
// code run on load
Object.values(Types).forEach(t => fillLibrary(t));
setInterval(() => fillLibrary(Types.reminder), 60000);
Object.values(reminderTypes).forEach(t => fillLibrary(t));
setInterval(() => fillLibrary(reminderTypes.reminder), 60000);
const week_days = ["Mo", "Tu", "We", "Thu", "Fr", "Sa", "Su"];

View File

@@ -1,139 +1,112 @@
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")
const els = {
switchButton: document.querySelector(".switch-button"),
login: {
form: document.querySelector('#login-form'),
inputContainers: {
username: document.querySelector('#login-form .checked-input-container:has(input[type="text"])'),
password: document.querySelector('#login-form .checked-input-container:has(input[type="password"])')
},
inputs: {
username: document.querySelector('#login-username-input'),
password: document.querySelector('#login-password-input')
}
},
'create': {
'form': document.querySelector('#create-form'),
'inputs': {
'username': document.querySelector('#create-form input[type="text"]'),
'password': document.querySelector('#create-form input[type="password"]')
create: {
form: document.querySelector('#register-form'),
inputContainers: {
username: document.querySelector('#register-form .checked-input-container:has(input[type="text"])'),
},
inputs: {
username: document.querySelector('#register-username-input'),
password: document.querySelector('#register-password-input')
},
'errors': {
'username_invalid': document.querySelector('#new-username-error'),
'username_taken': document.querySelector('#taken-username-error')
errors: {
usernameInvalid: document.querySelector('#invalid-username-error'),
usernameTaken: document.querySelector('#taken-username-error')
}
}
};
}
function login(data=null) {
forms.login.errors.username.classList.remove('error-container');
forms.login.errors.password.classList.remove('error-container');
function login(data = null) {
els.login.inputContainers.username.classList.remove('error-input-container')
els.login.inputContainers.password.classList.remove('error-input-container')
if (data === null)
data = {
'username': forms.login.inputs.username.value,
'password': forms.login.inputs.password.value
};
username: els.login.inputs.username.value,
password: els.login.inputs.password.value
}
fetch(`${url_prefix}/api/auth/login`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
})
.then(response => {
if (!response.ok) return Promise.reject(response.status);
return response.json();
})
sendAPI("POST", "/auth/login", {}, data, true, false)
.then(json => {
const new_stor = JSON.parse(localStorage.getItem('MIND'));
new_stor.api_key = json.result.api_key;
localStorage.setItem('MIND', JSON.stringify(new_stor));
setLocalStorage({api_key: json.result.api_key})
if (json.result.admin)
window.location.href = `${url_prefix}/admin`;
window.location.href = `${urlPrefix}/admin`
else
window.location.href = `${url_prefix}/reminders`;
window.location.href = `${urlPrefix}/reminders`
})
.catch(e => {
if (e === 404)
forms.login.errors.username.classList.add('error-container');
else if (e === 401)
forms.login.errors.password.classList.add('error-container');
if (e.status === 404)
els.login.inputContainers.username.classList.add('error-input-container')
else if (e.status === 401)
els.login.inputContainers.password.classList.add('error-input-container')
else
console.log(e);
});
};
console.log(e)
})
}
function create() {
forms.create.inputs.username.classList.remove('error-input');
forms.create.errors.username_invalid.classList.add('hidden');
forms.create.errors.username_taken.classList.add('hidden');
els.create.inputContainers.username.classList.remove('error-input-container')
hide([els.create.errors.usernameInvalid, els.create.errors.usernameTaken])
const data = {
'username': forms.create.inputs.username.value,
'password': forms.create.inputs.password.value
};
fetch(`${url_prefix}/api/user/add`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
})
.then(response => response.json())
.then(json => {
if (json.error !== null) return Promise.reject(json.error);
login(data);
})
.catch(e => {
if (e === 'UsernameInvalid') {
forms.create.inputs.username.classList.add('error-input');
forms.create.errors.username_invalid.classList.remove('hidden');
} else if (e === 'UsernameTaken') {
forms.create.inputs.username.classList.add('error-input');
forms.create.errors.username_taken.classList.remove('hidden');
} else {
console.log(e);
};
});
};
username: els.create.inputs.username.value,
password: els.create.inputs.password.value
}
function checkLogin() {
fetch(`${url_prefix}/api/auth/status?api_key=${api_key}`)
.then(response => {
if (!response.ok) return Promise.reject(response.status);
return response.json();
})
.then(json => {
if (json.result.admin)
window.location.href = `${url_prefix}/admin`;
else
window.location.href = `${url_prefix}/reminders`;
})
sendAPI("POST", "/user/add", {}, data)
.then(json => login(data))
.catch(e => {
if (e === 401)
console.log('API key expired')
else
console.log(e);
});
};
e.json().then(e => {
if (e.error === 'UsernameInvalid') {
els.create.errors.usernameInvalid.innerText = e.result.reason
hide([], [els.create.errors.usernameInvalid])
els.create.inputContainers.username.classList.add('error-input-container')
} else if (e.error === 'UsernameTaken') {
hide([], [els.create.errors.usernameTaken])
els.create.inputContainers.username.classList.add('error-input-container')
} else {
console.log(e)
}
})
})
}
function checkAllowNewAccounts() {
fetch(`${url_prefix}/api/settings`)
.then(response => response.json())
const cachedValue = getLocalStorage("allow_new_accounts_cache").allow_new_accounts_cache
if (!cachedValue)
hide([els.switchButton])
fetchAPI("/settings")
.then(json => {
if (!json.result.allow_new_accounts)
document.querySelector('.switch-button').classList.add('hidden');
});
};
hide([els.switchButton])
else
hide([], [els.switchButton])
// code run on load
if (cachedValue !== json.result.allow_new_accounts)
setLocalStorage({
allow_new_accounts_cache: json.result.allow_new_accounts
})
})
}
if (localStorage.getItem('MIND') === null)
localStorage.setItem('MIND', JSON.stringify(
{'api_key': null, 'locale': 'en-GB', 'default_service': null}
))
if (apiKey !== null)
checkLogin()
const url_prefix = document.getElementById('url_prefix').dataset.value;
const api_key = JSON.parse(localStorage.getItem('MIND')).api_key;
checkAllowNewAccounts()
checkLogin();
checkAllowNewAccounts();
forms.login.form.action = 'javascript:login();';
forms.create.form.action = 'javascript:create();';
els.login.form.action = 'javascript:login();'
els.create.form.action = 'javascript:create();'

View File

@@ -88,11 +88,11 @@ function setNoNotificationServiceMsg(json) {
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);
ae.onclick = e => showAdd(reminderTypes.reminder);
else if (ae.id === 'add-static-reminder')
ae.onclick = e => showAdd(Types.static_reminder);
ae.onclick = e => showAdd(reminderTypes.static_reminder);
else if (ae.id === 'add-template')
ae.onclick = e => showAdd(Types.template);
ae.onclick = e => showAdd(reminderTypes.template);
});
} else {
@@ -104,7 +104,7 @@ function setNoNotificationServiceMsg(json) {
};
function fillNotificationServices() {
fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`)
fetch(`${urlPrefix}/api/notificationservices?api_key=${apiKey}`)
.then(response => {
if (!response.ok) return Promise.reject(response.status);
return response.json();
@@ -116,7 +116,7 @@ function fillNotificationServices() {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else
console.log(e);
});
@@ -132,7 +132,7 @@ function saveService(id) {
'title': row.querySelector(`td.title-column > input`).value,
'url': row.querySelector(`td.url-column > input`).value
};
fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}`, {
fetch(`${urlPrefix}/api/notificationservices/${id}?api_key=${apiKey}`, {
'method': 'PUT',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -144,7 +144,7 @@ function saveService(id) {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else if (e === 400) {
save_button.classList.add('error-icon');
save_button.title = 'Invalid Apprise URL';
@@ -155,7 +155,7 @@ function saveService(id) {
function deleteService(id, delete_reminders_using=false) {
const row = document.querySelector(`tr[data-id="${id}"]`);
fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}&delete_reminders_using=${delete_reminders_using}`, {
fetch(`${urlPrefix}/api/notificationservices/${id}?api_key=${apiKey}&delete_reminders_using=${delete_reminders_using}`, {
'method': 'DELETE'
})
.then(response => response.json())
@@ -165,14 +165,14 @@ function deleteService(id, delete_reminders_using=false) {
row.remove();
fillNotificationServices();
if (delete_reminders_using) {
fillLibrary(Types.reminder);
fillLibrary(Types.static_reminder);
fillLibrary(Types.template);
fillLibrary(reminderTypes.reminder);
fillLibrary(reminderTypes.static_reminder);
fillLibrary(reminderTypes.template);
};
})
.catch(e => {
if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid')
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else if (e.error === 'NotificationServiceInUse') {
const delete_reminders = confirm(
@@ -200,7 +200,7 @@ function showServiceList(e) {
if (notification_services !== null)
return;
fetch(`${url_prefix}/api/notificationservices/available?api_key=${api_key}`)
fetch(`${urlPrefix}/api/notificationservices/available?api_key=${apiKey}`)
.then(response => response.json())
.then(json => {
notification_services = json.result;
@@ -322,7 +322,7 @@ function createEntriesList(token) {
const add_button = document.createElement('button');
add_button.type = 'button';
add_button.innerHTML = Icons.add;
add_button.innerHTML = icons.add;
add_button.onclick = e => toggleAddRow(add_row);
entries_list.appendChild(add_button);
@@ -568,7 +568,7 @@ function testService() {
test_button.title = 'Required field missing';
return;
};
fetch(`${url_prefix}/api/notificationservices/test?api_key=${api_key}`, {
fetch(`${urlPrefix}/api/notificationservices/test?api_key=${apiKey}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -582,7 +582,7 @@ function testService() {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else if (e === 400) {
test_button.classList.add('error-input');
test_button.title = 'Invalid Apprise URL';
@@ -624,7 +624,7 @@ function addService() {
add_button.title = 'Required field missing';
return;
};
fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`, {
fetch(`${urlPrefix}/api/notificationservices?api_key=${apiKey}`, {
'method': 'POST',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -641,7 +641,7 @@ function addService() {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else if (e === 400) {
add_button.classList.add('error-input');
add_button.title = 'Invalid Apprise URL';

View File

@@ -0,0 +1,38 @@
const reminderTypes = {
reminder: document.getElementById('reminder-tab'),
static_reminder: document.getElementById('static-reminder-tab'),
template: document.getElementById('template-tab')
}
const infoClasses = [
'show-add-reminder', 'show-add-template', 'show-add-static-reminder',
'show-edit-reminder', 'show-edit-template', 'show-edit-static-reminder'
]
function showWindow(id) {
const window_container = document.querySelector('.window-container')
if (id === "home") {
window_container.classList.remove(
'show-window',
'inter-window-ani'
)
} 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'),
constants.windowAnimationDuration
)
}
}
checkLogin()

View File

@@ -13,7 +13,7 @@ function loadSettings() {
function updateLocale(e) {
setLocalStorage({'locale': e.target.value});
fillLibrary(Types.reminder);
fillLibrary(reminderTypes.reminder);
};
function updateDefaultService(e) {
@@ -25,7 +25,7 @@ function changePassword() {
const data = {
'new_password': document.getElementById('password-input').value
};
fetch(`${url_prefix}/api/user?api_key=${api_key}`, {
fetch(`${urlPrefix}/api/user?api_key=${apiKey}`, {
'method': 'PUT',
'headers': {'Content-Type': 'application/json'},
'body': JSON.stringify(data)
@@ -36,18 +36,18 @@ function changePassword() {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else
console.log(e);
});
};
function deleteAccount() {
fetch(`${url_prefix}/api/user?api_key=${api_key}`, {
fetch(`${urlPrefix}/api/user?api_key=${apiKey}`, {
'method': 'DELETE'
})
.then(response => {
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
});
};

View File

@@ -19,21 +19,21 @@ function showAdd(type) {
const cl = document.getElementById('info').classList;
cl.forEach(c => {
if (InfoClasses.includes(c)) cl.remove(c)
if (infoClasses.includes(c)) cl.remove(c)
});
document.querySelector('.options > button[type="submit"]').innerText = 'Add';
document.querySelector('#test-reminder > div:first-child').innerText = 'Test';
const title = document.querySelector('#info h2');
if (type === Types.reminder) {
if (type === reminderTypes.reminder) {
cl.add('show-add-reminder');
title.innerText = 'Add a reminder';
inputs.time.setAttribute('required', '');
} else if (type === Types.template) {
} else if (type === reminderTypes.template) {
cl.add('show-add-template');
title.innerText = 'Add a template';
inputs.time.removeAttribute('required');
} else if (type === Types.static_reminder) {
} else if (type === reminderTypes.static_reminder) {
cl.add('show-add-static-reminder');
title.innerText = 'Add a static reminder';
inputs.time.removeAttribute('required');
@@ -44,15 +44,15 @@ function showAdd(type) {
function showEdit(id, type) {
let url;
if (type === Types.reminder) {
url = `${url_prefix}/api/reminders/${id}?api_key=${api_key}`;
if (type === reminderTypes.reminder) {
url = `${urlPrefix}/api/reminders/${id}?api_key=${apiKey}`;
inputs.time.setAttribute('required', '');
} else if (type === Types.template) {
url = `${url_prefix}/api/templates/${id}?api_key=${api_key}`;
} else if (type === reminderTypes.template) {
url = `${urlPrefix}/api/templates/${id}?api_key=${apiKey}`;
inputs.time.removeAttribute('required');
type_buttons.repeat_interval.removeAttribute('required');
} else if (type === Types.static_reminder) {
url = `${url_prefix}/api/staticreminders/${id}?api_key=${api_key}`;
} else if (type === reminderTypes.static_reminder) {
url = `${urlPrefix}/api/staticreminders/${id}?api_key=${apiKey}`;
document.getElementById('test-reminder').classList.remove('show-sent');
inputs.time.removeAttribute('required');
type_buttons.repeat_interval.removeAttribute('required');
@@ -69,7 +69,7 @@ function showEdit(id, type) {
selectColor(json.result.color || colors[0]);
inputs.title.value = json.result.title;
if (type === Types.reminder) {
if (type === reminderTypes.reminder) {
inputs.enabled.checked = json.result.enabled;
var trigger_date = new Date(
(json.result.time
@@ -86,7 +86,7 @@ function showEdit(id, type) {
c => c.checked = json.result.notification_services.includes(parseInt(c.dataset.id))
);
if (type == Types.reminder) {
if (type == reminderTypes.reminder) {
if (json.result.repeat_interval !== null) {
toggleRepeated();
type_buttons.repeat_interval.value = json.result.repeat_interval;
@@ -107,27 +107,27 @@ function showEdit(id, type) {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else
console.log(e);
});
const cl = document.getElementById('info').classList;
cl.forEach(c => {
if (InfoClasses.includes(c)) cl.remove(c)
if (infoClasses.includes(c)) cl.remove(c)
});
document.querySelector('.options > button[type="submit"]').innerText = 'Save';
const title = document.querySelector('#info h2');
const test_text = document.querySelector('#test-reminder > div:first-child');
if (type === Types.reminder) {
if (type === reminderTypes.reminder) {
cl.add('show-edit-reminder');
title.innerText = 'Edit a reminder';
test_text.innerText = 'Test';
} else if (type === Types.template) {
} else if (type === reminderTypes.template) {
cl.add('show-edit-template');
title.innerText = 'Edit a template';
test_text.innerText = 'Test';
} else if (type === Types.static_reminder) {
} else if (type === reminderTypes.static_reminder) {
cl.add('show-edit-static-reminder');
title.innerText = 'Edit a static reminder';
test_text.innerText = 'Trigger';

View File

@@ -1,5 +1,5 @@
function loadTemplateSelection() {
fetch(`${url_prefix}/api/templates?api_key=${api_key}`)
fetch(`${urlPrefix}/api/templates?api_key=${apiKey}`)
.then(response => {
if (!response.ok) return Promise.reject(response.status);
return response.json();
@@ -17,7 +17,7 @@ function loadTemplateSelection() {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else
console.log(e);
});
@@ -33,7 +33,7 @@ function applyTemplate() {
selectColor(colors[0]);
} else {
fetch(`${url_prefix}/api/templates/${inputs.template.value}?api_key=${api_key}`)
fetch(`${urlPrefix}/api/templates/${inputs.template.value}?api_key=${apiKey}`)
.then(response => {
if (!response.ok) return Promise.reject(response.status);
else return response.json();
@@ -48,7 +48,7 @@ function applyTemplate() {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else
console.log(e);
});

View File

@@ -102,7 +102,7 @@ function testReminder() {
let url;
if (cl.contains('show-edit-static-reminder')) {
// Trigger static reminder
url = `${url_prefix}/api/staticreminders/${r_id}?api_key=${api_key}`;
url = `${urlPrefix}/api/staticreminders/${r_id}?api_key=${apiKey}`;
} else {
// Test reminder draft
if (inputs.title.value === '') {
@@ -132,7 +132,7 @@ function testReminder() {
'text': inputs.text.value
};
headers.body = JSON.stringify(data);
url = `${url_prefix}/api/reminders/test?api_key=${api_key}`;
url = `${urlPrefix}/api/reminders/test?api_key=${apiKey}`;
};
fetch(url, headers)
.then(response => {
@@ -141,7 +141,7 @@ function testReminder() {
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else
console.log(e);
});
@@ -153,13 +153,13 @@ function deleteInfo() {
const cl = document.getElementById('info').classList;
if (cl.contains('show-edit-reminder')) {
// Delete reminder
url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`;
url = `${urlPrefix}/api/reminders/${e_id}?api_key=${apiKey}`;
} else if (cl.contains('show-edit-template')) {
// Delete template
url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`;
url = `${urlPrefix}/api/templates/${e_id}?api_key=${apiKey}`;
} else if (cl.contains('show-edit-static-reminder')) {
// Delete static reminder
url = `${url_prefix}/api/staticreminders/${e_id}?api_key=${api_key}`;
url = `${urlPrefix}/api/staticreminders/${e_id}?api_key=${apiKey}`;
} else return;
fetch(url, {'method': 'DELETE'})
@@ -168,20 +168,20 @@ function deleteInfo() {
if (cl.contains('show-edit-reminder')) {
// Delete reminder
fillLibrary(Types.reminder);
fillLibrary(reminderTypes.reminder);
} else if (cl.contains('show-edit-template')) {
// Delete template
fillLibrary(Types.template);
fillLibrary(reminderTypes.template);
loadTemplateSelection();
} else if (cl.contains('show-edit-static-reminder')) {
// Delete static reminder
fillLibrary(Types.static_reminder);
fillLibrary(reminderTypes.static_reminder);
};
showWindow("home");
})
.catch(e => {
if (e === 401)
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
else
console.log(e);
});
@@ -244,24 +244,24 @@ function submitInfo() {
};
};
fetch_data.url = `${url_prefix}/api/reminders?api_key=${api_key}`;
fetch_data.url = `${urlPrefix}/api/reminders?api_key=${apiKey}`;
fetch_data.method = 'POST';
fetch_data.call_back = () => fillLibrary(Types.reminder);
fetch_data.call_back = () => fillLibrary(reminderTypes.reminder);
} else if (cl.contains('show-add-template')) {
// Add template
fetch_data.url = `${url_prefix}/api/templates?api_key=${api_key}`;
fetch_data.url = `${urlPrefix}/api/templates?api_key=${apiKey}`;
fetch_data.method = 'POST';
fetch_data.call_back = () => {
loadTemplateSelection();
fillLibrary(Types.template);
fillLibrary(reminderTypes.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.url = `${urlPrefix}/api/staticreminders?api_key=${apiKey}`;
fetch_data.method = 'POST';
fetch_data.call_back = () => fillLibrary(Types.static_reminder);
fetch_data.call_back = () => fillLibrary(reminderTypes.static_reminder);
} else if (cl.contains('show-edit-reminder')) {
// Edit reminder
@@ -289,24 +289,24 @@ function submitInfo() {
};
};
fetch_data.url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`;
fetch_data.url = `${urlPrefix}/api/reminders/${e_id}?api_key=${apiKey}`;
fetch_data.method = 'PUT';
fetch_data.call_back = () => fillLibrary(Types.reminder);
fetch_data.call_back = () => fillLibrary(reminderTypes.reminder);
} else if (cl.contains('show-edit-template')) {
// Edit template
fetch_data.url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`;
fetch_data.url = `${urlPrefix}/api/templates/${e_id}?api_key=${apiKey}`;
fetch_data.method = 'PUT';
fetch_data.call_back = () => {
loadTemplateSelection();
fillLibrary(Types.template);
fillLibrary(reminderTypes.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.url = `${urlPrefix}/api/staticreminders/${e_id}?api_key=${apiKey}`;
fetch_data.method = 'PUT';
fetch_data.call_back = () => fillLibrary(Types.static_reminder);
fetch_data.call_back = () => fillLibrary(reminderTypes.static_reminder);
} else return;
@@ -323,7 +323,7 @@ function submitInfo() {
})
.catch(e => {
if (e === 401) {
window.location.href = `${url_prefix}/`;
window.location.href = `${urlPrefix}/`;
} else if (e === 400) {
inputs.time.classList.add('error-input');
inputs.time.title = 'Time is in the past';

View File

@@ -25,40 +25,265 @@
<img src="{{ url_for('static', filename='img/favicon.svg') }}" alt="">
</header>
<main>
<section class="action-buttons">
<button id="save-button" title="Save Settings" type="submit" form="settings-form">
<dialog id="reset-settings-dialog">
<div class="dialog-container">
<div class="dialog-header">
<h2>Reset Settings</h2>
</div>
<div class="dialog-content">
<form id="reset-settings-form">
<div class="table-container">
<table id="reset-table" class="entries-table">
<tbody id="reset-list">
<tr>
<td><input type="checkbox" data-setting="allow_new_accounts"></td>
<td>Allow New Accounts</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="login_time"></td>
<td>Login Time</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="login_time_reset"></td>
<td>Login Time Trigger</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="host"></td>
<td>Host</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="port"></td>
<td>Port</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="url_prefix"></td>
<td>URL Prefix</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="log_level"></td>
<td>Logging Level</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="db_backup_interval"></td>
<td>Database Backup Interval</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="db_backup_amount"></td>
<td>Database Backup Retention</td>
</tr>
<tr>
<td><input type="checkbox" data-setting="db_backup_folder"></td>
<td>Database Backup Folder</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
<div class="dialog-footer">
<button id="close-reset-settings" class="input-style">Cancel</button>
<button type="submit" id="submit-reset-button" form="reset-settings-form" class="input-style">Reset</button>
</div>
</div>
</dialog>
<dialog id="add-user-dialog">
<div class="dialog-container">
<div class="dialog-header">
<h2>Add User</h2>
</div>
<div class="dialog-content">
<form id="add-user-form">
<div class="checked-input-container">
<input type="text" id="add-user-username-input" autocomplete="username" placeholder="Username" class="input-style" required>
<p id="add-invalid-username-error">Username invalid</p>
<p id="add-taken-username-error">Username already taken</p>
</div>
<input type="password" id="add-user-password-input" autocomplete="new-password" placeholder="Password" class="input-style" required>
</form>
</div>
<div class="dialog-footer">
<button id="close-add-user" class="input-style">Cancel</button>
<button type="submit" form="add-user-form" class="input-style">Add</button>
</div>
</div>
</dialog>
<dialog id="edit-user-dialog">
<div class="dialog-container">
<div class="dialog-header">
<h2>Edit User</h2>
</div>
<div class="dialog-content">
<p>Leave fields empty to not change for <span id="username-edit-user"></span></p>
<form id="edit-user-form">
<div class="checked-input-container">
<input type="text" id="edit-user-username-input" autocomplete="username" placeholder="New username" class="input-style">
<p id="edit-invalid-username-error">Username invalid</p>
<p id="edit-taken-username-error">Username already taken</p>
</div>
<input type="password" id="edit-user-password-input" autocomplete="new-password" placeholder="New password" class="input-style">
</form>
</div>
<div class="dialog-footer">
<button id="close-edit-user" class="input-style">Cancel</button>
<button type="submit" form="edit-user-form" class="input-style">Save</button>
</div>
</div>
</dialog>
<dialog id="delete-user-dialog">
<div class="dialog-container">
<div class="dialog-content">
<p>Are you sure you want to delete the user <span id="username-delete-user"></span>?</p>
<div class="confirm-container">
<button id="confirm-delete-user" class="input-style">Confirm</button>
<button id="close-delete-user" class="input-style">Cancel</button>
</div>
</div>
</div>
</dialog>
<dialog id="upload-db-dialog">
<div class="dialog-container">
<div class="dialog-header">
<h2>Upload Database</h2>
</div>
<div class="dialog-content">
<p>You will be uploading a database file. Login into MIND within one minute to keep the new database, or the upload will automatically be reverted.</p>
<form id="upload-db-form">
<div class="table-container">
<table class="collaps-table">
<tbody>
<tr>
<td><label for="database-file-input">Database File</label></td>
<td>
<div class="checked-input-container">
<input type="file" id="database-file-input" class="input-style" required>
<p>Invalid database file</p>
</div>
</td>
</tr>
<tr>
<td><label for="copy-hosting-input-upload">Keep Hosting Settings</label></td>
<td>
<input type="checkbox" id="copy-hosting-input-upload">
<p>Keep the current hosting settings instead of using the settings in the uploaded database when importing.</p>
</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
<div class="dialog-footer">
<button id="close-upload-db" class="input-style">Cancel</button>
<button type="submit" form="upload-db-form" class="input-style">Import</button>
</div>
</div>
</dialog>
<dialog id="import-db-dialog">
<div class="dialog-container">
<div class="dialog-header">
<h2>Import Database</h2>
</div>
<div class="dialog-content">
<p>You will be importing database backup <span id="db-backup-name"></span>, created on <span id="db-creation-date"></span>.
Login into MIND within one minute to keep the new database, or the import will automatically be reverted.</p>
<form id="import-db-form">
<div class="table-container">
<table class="collaps-table">
<tbody>
<tr>
<td><label for="copy-hosting-input-import">Keep Hosting Settings</label></td>
<td>
<input type="checkbox" id="copy-hosting-input-import">
<p>Keep the current hosting settings instead of using the settings in the selected database backup when importing.</p>
</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
<div class="dialog-footer">
<button id="close-import-db" class="input-style">Cancel</button>
<button type="submit" form="import-db-form" class="input-style">Import</button>
</div>
</div>
</dialog>
<form id="settings-form"></form>
<section class="action-buttons" aria-label="Actions">
<button id="save-button" aria-label="Save settings" title="Save Settings" type="submit" form="settings-form">
<span id="changes-count" data-count="0">0 changes</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512">
<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 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"/>
</svg>
</button>
<button id="logout-button" aria-label="Log out of MIND" title="Logout">
<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">
<button id="logout-button" aria-label="Log out of MIND" title="Log out of MIND">
<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" xml:space="preserve">
<g>
<path d="M11.476,15a1,1,0,0,0-1,1v3a3,3,0,0,1-3,3H5a3,3,0,0,1-3-3V5A3,3,0,0,1,5,2H7.476a3,3,0,0,1,3,3V8a1,1,0,0,0,2,0V5a5.006,5.006,0,0,0-5-5H5A5.006,5.006,0,0,0,0,5V19a5.006,5.006,0,0,0,5,5H7.476a5.006,5.006,0,0,0,5-5V16A1,1,0,0,0,11.476,15Z"></path>
<path d="M22.867,9.879,18.281,5.293a1,1,0,1,0-1.414,1.414l4.262,4.263L6,11a1,1,0,0,0,0,2H6l15.188-.031-4.323,4.324a1,1,0,1,0,1.414,1.414l4.586-4.586A3,3,0,0,0,22.867,9.879Z"></path>
</g>
</svg>
<span>Log out</span>
</button>
</section>
<div class="form-container">
<form id="settings-form">
<h2>Authentication</h2>
<div class="settings-table-container">
<table class="settings-table">
<div id="grid-container">
<section id="about-section">
<h2>About</h2>
<div class="table-container">
<table class="collaps-table">
<tbody>
<tr>
<td><label for="allow-new-accounts-input">Allow New Accounts</label></td>
<th>MIND Version</th>
<td id="mind-version"></td>
</tr>
<tr>
<th>Python Version</th>
<td id="python-version"></td>
</tr>
<tr>
<th>Database Version</th>
<td id="db-version"></td>
</tr>
<tr>
<th>Database Location</th>
<td id="db-location"></td>
</tr>
<tr>
<th>Data Folder</th>
<td id="data-folder"></td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="power-section">
<h2>Power</h2>
<div class="table-container">
<button id="restart-button" class="input-style">Restart</button>
<button id="shutdown-button" class="input-style">Shutdown</button>
</div>
</section>
<section id="auth-section">
<h2>Authentication</h2>
<div class="table-container">
<table class="collaps-table">
<tbody>
<tr>
<th><label for="allow-new-accounts-input">Allow New Accounts</label></th>
<td>
<input type="checkbox" id="allow-new-accounts-input">
<input type="checkbox" id="allow-new-accounts-input" form="settings-form">
<p>Allow users to register a new account. The admin can always add a new account from this panel.</p>
</td>
</tr>
<tr>
<td><label for="login-time-input">Login Time</label></td>
<th><label for="login-time-input">Login Time</label></th>
<td>
<div class="number-input">
<input type="number" id="login-time-input" min="1" max="43200" required>
<div class="input-style">
<input type="number" id="login-time-input" form="settings-form" min="1" max="43200" required>
<p>Min</p>
</div>
<p>For how long users stay logged in before having to authenticate again. Between 1 minute and 1 month.</p>
@@ -66,9 +291,9 @@
</td>
</tr>
<tr>
<td><label for="login-time-reset-input">Login Time Trigger</label></td>
<th><label for="login-time-reset-input">Login Time Trigger</label></th>
<td>
<select id="login-time-reset-input">
<select id="login-time-reset-input" class="input-style" form="settings-form" required>
<option value="true">After Last Use</option>
<option value="false">After Login</option>
</select>
@@ -78,195 +303,163 @@
</tbody>
</table>
</div>
<h2>Logging</h2>
<div class="settings-table-container">
<table class="settings-table">
</section>
<section id="hosting-section">
<h2>Hosting</h2>
<div class="table-container">
<table class="collaps-table">
<tbody>
<tr>
<td><label for="log-level-input">Logging Level</label></td>
<th><label for="host-input">Host</label></th>
<td>
<select id="log-level-input">
<option value="20">Info</option>
<option value="10">Debug</option>
</select>
<input type="text" id="host-input" class="input-style" form="settings-form" spellcheck="false" required>
<p>Valid IPv4 address (default is '0.0.0.0' for all available interfaces).</p>
</td>
</tr>
<tr>
<th><label for="port-input">Port</label></th>
<td>
<input type="number" id="port-input" class="input-style" form="settings-form" min="1" max="65535" required>
<p>The port used to access the web UI (default is '8080').</p>
</td>
</tr>
<tr>
<th><label for="url-prefix-input">URL Prefix</label></th>
<td>
<input type="text" id="url-prefix-input" class="input-style" form="settings-form" spellcheck="false">
<p>For reverse proxy support (default is empty).</p>
</td>
</tr>
</tbody>
</table>
</div>
<div class="database-container">
<button id="download-logs-button" type="button">Download Debug Logs</button>
</div>
<h2>Database Backups</h2>
<div class="settings-table-container">
<table class="settings-table">
</section>
<section id="logging-section">
<h2>Logging and Resetting</h2>
<div class="table-container">
<table class="collaps-table">
<tbody>
<tr>
<td><label for="db-backup-interval-input">Database Backup Interval</label></td>
<th><label for="log-level-input">Logging Level</label></th>
<td>
<div class="number-input">
<input type="number" id="db-backup-interval-input" min="1" required>
<select id="log-level-input" class="input-style" form="settings-form" required>
<option value="20">Info</option>
<option value="10">Debug</option>
</select>
</td>
</tr>
<tr>
<th><label for="download-logs-button">Download Logs</label></th>
<td>
<button id="download-logs-button" class="input-style">Download Debug Logs</button>
</td>
</tr>
<tr>
<th><label for="open-reset-button">Reset Setting</label></th>
<td>
<button id="open-reset-button" class="input-style">Reset Setting</button>
<p>Opens a window to select settings for which to reset their value.</p>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="backup-settings-section">
<h2>Database Backup Settings</h2>
<div class="table-container">
<table class="collaps-table">
<tbody>
<tr>
<th><label for="db-backup-interval-input">Database Backup Interval</label></th>
<td>
<div class="input-style">
<input type="number" id="db-backup-interval-input" form="settings-form" min="1" required>
<p>Hours</p>
</div>
<p>How often to make a backup of the database.</p>
</td>
</tr>
<tr>
<td><label for="db-backup-amount-input">Database Backup Retention</label></td>
<th><label for="db-backup-amount-input">Database Backup Retention</label></th>
<td>
<div class="number-input">
<input type="number" id="db-backup-amount-input" min="1" required>
<div class="input-style">
<input type="number" id="db-backup-amount-input" form="settings-form" min="1" required>
<p>Backups</p>
</div>
<p>How many backups to keep. The oldest one will be removed if needed.</p>
</td>
</tr>
<tr>
<td><label for="db-backup-folder-input">Database Backup Folder</label></td>
<th><label for="db-backup-folder-input">Database Backup Folder</label></th>
<td>
<input type="text" id="db-backup-folder-input" required>
<div class="checked-input-container">
<input type="text" id="db-backup-folder-input" class="input-style long-input-style" form="settings-form" spellcheck="false" required>
<p>Path doesn't exist or isn't a folder</p>
</div>
<p>The folder to store the backups in.</p>
</td>
</tr>
</tbody>
</table>
</div>
</form>
<form id="hosting-form">
<h2>Hosting</h2>
<div class="settings-table-container">
<table class="settings-table">
<tbody>
<tr>
<td><label for="host-input">Host</label></td>
<td>
<input type="text" id="host-input" required>
<p>Valid IPv4 address (default is '0.0.0.0' for all available interfaces).</p>
</td>
</tr>
<tr>
<td><label for="port-input">Port</label></td>
<td>
<input type="number" id="port-input" min="1" max="65535" required>
<p>The port used to access the web UI (default is '8080').</p>
</td>
</tr>
<tr>
<td><label for="url-prefix-input">URL Prefix</label></td>
<td>
<input type="text" id="url-prefix-input">
<p>For reverse proxy support (default is empty).</p>
</td>
</tr>
</tbody>
</table>
<button type="submit" id="save-hosting-button">Save and Restart</button>
</div>
<p>IMPORTANT: Login into MIND within one minute to keep the new database, or the import will automatically be reverted.
See <a href="https://casvt.github.io/MIND/general_info/admin_panel#database">the documentation</a> for more information.</p>
</form>
<h2>User Management</h2>
<div class="add-user-container">
<button id="add-user-button">
<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>
</section>
<section id="user-section">
<h2>User Management</h2>
<div class="add-item-container">
<button id="add-user-button" class="input-style">
<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" 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>
<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>
</g>
</svg>
</button>
</div>
<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">
<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>
</table>
</div>
<h2>Database</h2>
<form id="upload-database-form">
<div class="settings-table-container">
<table class="settings-table">
<tbody>
<tr>
<td><label for="copy-hosting-input">Keep Hosting Settings</label></td>
<td>
<input type="checkbox" id="copy-hosting-input">
<p>Keep the current hosting settings instead of using the settings in the uploaded database when importing.</p>
</td>
</tr>
<tr>
<td><label for="database-file-input">Database File</label></td>
<td>
<input type="file" id="database-file-input" required>
<p>Instead of importing a backup, import a database by uploading the file.</p>
</td>
</tr>
</svg>
</button>
</div>
<div class="table-container">
<table id="user-table" class="entries-table">
<thead>
<th>User</th>
<th>Actions</th>
</thead>
<tbody id="user-list">
</tbody>
</table>
</div>
<button type="submit" id="upload-db-button">Import Database</button>
<p>IMPORTANT: Login into MIND within one minute to keep the new database, or the import will automatically be reverted.
See <a href="https://casvt.github.io/MIND/general_info/admin_panel#database">the documentation</a> for more information.</p>
</form>
<div class="settings-table-container">
<table id="backup-table">
<thead>
<th>File</th>
<th>Creation</th>
<th>Actions</th>
</thead>
<tbody>
<tr>
<td>Current Database</td>
<td></td>
<td>
<button id="download-db-button">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512" height="512">
<g>
<path d="M210.731,386.603c24.986,25.002,65.508,25.015,90.51,0.029c0.01-0.01,0.019-0.019,0.029-0.029l68.501-68.501 c7.902-8.739,7.223-22.23-1.516-30.132c-8.137-7.357-20.527-7.344-28.649,0.03l-62.421,62.443l0.149-329.109 C277.333,9.551,267.782,0,256,0l0,0c-11.782,0-21.333,9.551-21.333,21.333l-0.192,328.704L172.395,288 c-8.336-8.33-21.846-8.325-30.176,0.011c-8.33,8.336-8.325,21.846,0.011,30.176L210.731,386.603z" />
<path d="M490.667,341.333L490.667,341.333c-11.782,0-21.333,9.551-21.333,21.333V448c0,11.782-9.551,21.333-21.333,21.333H64 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0C9.551,341.333,0,350.885,0,362.667V448 c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512,350.885,502.449,341.333,490.667,341.333z" />
</g>
</svg>
</button>
</td>
</tr>
</tbody>
<tbody id="backup-list"></tbody>
</table>
</div>
<h2>Power</h2>
<div class="database-container">
<button id="restart-button">Restart</button>
<button id="shutdown-button">Shutdown</button>
</div>
</section>
<section id="backup-management-section">
<h2>Database Backup Management</h2>
<div class="add-item-container">
<button id="upload-db-button" class="input-style">Upload Database</button>
</div>
<div class="table-container">
<table id="backup-table" class="entries-table">
<thead>
<th>File</th>
<th>Creation</th>
<th>Actions</th>
</thead>
<tbody>
<tr>
<td>Current Database</td>
<td></td>
<td>
<button id="download-db-button" title="Download current database">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512" height="512">
<g>
<path d="M210.731,386.603c24.986,25.002,65.508,25.015,90.51,0.029c0.01-0.01,0.019-0.019,0.029-0.029l68.501-68.501 c7.902-8.739,7.223-22.23-1.516-30.132c-8.137-7.357-20.527-7.344-28.649,0.03l-62.421,62.443l0.149-329.109 C277.333,9.551,267.782,0,256,0l0,0c-11.782,0-21.333,9.551-21.333,21.333l-0.192,328.704L172.395,288 c-8.336-8.33-21.846-8.325-30.176,0.011c-8.33,8.336-8.325,21.846,0.011,30.176L210.731,386.603z" />
<path d="M490.667,341.333L490.667,341.333c-11.782,0-21.333,9.551-21.333,21.333V448c0,11.782-9.551,21.333-21.333,21.333H64 c-11.782,0-21.333-9.551-21.333-21.333v-85.333c0-11.782-9.551-21.333-21.333-21.333l0,0C9.551,341.333,0,350.885,0,362.667V448 c0,35.346,28.654,64,64,64h384c35.346,0,64-28.654,64-64v-85.333C512,350.885,502.449,341.333,490.667,341.333z" />
</g>
</svg>
</button>
</td>
</tr>
</tbody>
<tbody id="backup-list"></tbody>
</table>
</div>
</section>
</div>
</main>
</body>

View File

@@ -15,6 +15,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/general.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
<script src="{{ url_for('static', filename='js/general.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
<title>Login - MIND</title>
@@ -26,40 +27,42 @@
<main>
<input type="checkbox" id="form-switch" class="hidden">
<div class="form-container">
<noscript>Javascript is disabled. The web-ui of MIND does not work with JavaScript disabled.</noscript>
<div class="login-container">
<form id="login-form">
<h2>Login</h2>
<noscript>Javascript is disabled. The web-ui of MIND does not work with JavaScript disabled.</noscript>
<div id="username-error-container">
<input type="text" autocomplete="username" placeholder="Username" required autofocus>
<p class="hidden">*Username not found</p>
<div class="checked-input-container">
<input type="text" id="login-username-input" autocomplete="username" placeholder="Username" class="input-style" required autofocus>
<p>Username not found</p>
</div>
<div id="password-error-container">
<input type="password" autocomplete="current-password" placeholder="Password" required>
<p class="hidden">*Password incorrect</p>
<div class="checked-input-container">
<input type="password" id="login-password-input" autocomplete="current-password" placeholder="Password" class="input-style" required>
<p>Password incorrect</p>
</div>
<label for="form-switch" class="switch-button">Or create an account</label>
<button type="submit">Login</button>
</form>
</div>
<div class="form-container">
<form id="create-form">
<h2>Create</h2>
<form id="register-form">
<h2>Register</h2>
<input type="text" autocomplete="username" placeholder="Username" required>
<p class="error hidden" id="new-username-error">*Username invalid</p>
<p class="error hidden" id="taken-username-error">*Username already taken</p>
<div class="checked-input-container">
<input type="text" id="register-username-input" autocomplete="username" placeholder="Username" class="input-style" required>
<p id="invalid-username-error">Username invalid</p>
<p id="taken-username-error">Username already taken</p>
</div>
<input type="password" autocomplete="new-password" placeholder="Password" required>
<input type="password" id="register-password-input" autocomplete="new-password" placeholder="Password" class="input-style" required>
<label for="form-switch" class="switch-button">Or log into an account</label>
<button type="submit">Create</button>
<button type="submit">Register</button>
</form>
<div id="form-cover">
<img src="{{ url_for('static', filename='img/favicon.svg') }}" alt="">
</div>
</div>
</main>
</body>
</html>
</html>

View File

@@ -14,11 +14,13 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/x-icon">
<link rel="stylesheet" href="{{ url_for('static', filename='css/general.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/reminders.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/info.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/library.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/notification.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}">
<script src="{{ url_for('static', filename='js/general.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/reminders.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/library.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/window.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/templates.js') }}" defer></script>
@@ -142,7 +144,7 @@
<noscript>Javascript is disabled. The web-ui of MIND does not work with JavaScript disabled.</noscript>
<form id="search-form">
<div class="search-bar">
<button type="submit">
<button type="submit" class="input-style">
<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 513.749 513.749" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
@@ -151,8 +153,8 @@
</g>
</svg>
</button>
<input type="text" id="search-input" required placeholder="Search..." aria-placeholder="Search for reminders">
<button type="button" id="clear-button">
<input type="text" id="search-input" class="input-style" required placeholder="Search..." aria-placeholder="Search for reminders">
<button type="button" id="clear-button" class="input-style">
<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.021 512.021" style="enable-background:new 0 0 512 512" xml:space="preserve">
<g>
<g>
@@ -161,7 +163,7 @@
</g>
</svg>
</button>
<select id="sort-input">
<select id="sort-input" class="input-style">
<option value="time">Time</option>
<option value="time_reversed">Time Reversed</option>
<option value="title">Title</option>